<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  
  
  <title>test | Hexo</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta name="description" content="[toc] JAVAJAVA基础上成员变量与局部变量的区别？ 语法形式 ：从语法形式上看，成员变量是属于类的，而局部变量是在代码块或方法中定义的变量或是方法的参数；成员变量可以被 public,private,static 等修饰符所修饰，而局部变量不能被访问控制修饰符及 static 所修饰；但是，成员变量和局部变量都能被 final 所修饰。 存储方式 ：从变量在内存中的存储方式来看,如果成员">
<meta property="og:type" content="article">
<meta property="og:title" content="test">
<meta property="og:url" content="https://gitee.com/xxditem/2023/04/11/%E9%9D%A2%E8%AF%95/index.html">
<meta property="og:site_name" content="Hexo">
<meta property="og:description" content="[toc] JAVAJAVA基础上成员变量与局部变量的区别？ 语法形式 ：从语法形式上看，成员变量是属于类的，而局部变量是在代码块或方法中定义的变量或是方法的参数；成员变量可以被 public,private,static 等修饰符所修饰，而局部变量不能被访问控制修饰符及 static 所修饰；但是，成员变量和局部变量都能被 final 所修饰。 存储方式 ：从变量在内存中的存储方式来看,如果成员">
<meta property="og:locale" content="en_US">
<meta property="og:image" content="g:\GitDocument\Study\%E9%9D%A2%E8%AF%95.assets\image-20230105193531104.png">
<meta property="og:image" content="https://oss.javaguide.cn/github/javaguide/java/basis/shallow&deep-copy.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230406183830873.png">
<meta property="og:image" content="https://s2.loli.net/2023/04/06/cgJWzxslyOhSQ8P.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/basis/spi/1ebd1df862c34880bc26b9d494535b3dtplv-k3u1fbpfcp-watermark.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230123153850477.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230123164932422.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230123165220480.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230123165507701.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230123170648107.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230123170752923.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230124200833608.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/collection/java-collection-hierarchy.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230125164213075.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230125164300381.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/collection/treemap_hierarchy.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230126114609538.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230126114539224.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230126152242606.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230126154455693.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230129111326493.png">
<meta property="og:image" content="https://oss.javaguide.cn/github/javaguide/java/concurrent/synchronized-get-lock-code-block.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230131152857687.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230131163042871.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230201195258399.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230202151006306.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230202160427504.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230203155940860.png">
<meta property="og:image" content="https://javaguide.cn/assets/threadpoolexecutor%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0.d54a5992.png">
<meta property="og:image" content="https://javaguide.cn/assets/24.ec7f7610.png">
<meta property="og:image" content="https://javaguide.cn/assets/27.9c78c2a2.png">
<meta property="og:image" content="https://javaguide.cn/assets/28.ea7d5196.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/jvm/java-runtime-data-areas-jdk1.8.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230207142812717.png">
<meta property="og:image" content="https://img-blog.csdnimg.cn/75930bcb2b174aa5b63027ae9982242a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LuO6I-c6bif5Yiw5pS-5byD,size_20,color_FFFFFF,t_70,g_se,x_16">
<meta property="og:image" content="https://img-blog.csdnimg.cn/b58dd3ca6ce04c119db8b64b8bc56623.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LuO6I-c6bif5Yiw5pS-5byD,size_20,color_FFFFFF,t_70,g_se,x_16">
<meta property="og:image" content="https://img-blog.csdnimg.cn/20200603000502908.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/jvm/access-location-of-object-handle.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/jvm/access-location-of-object-handle-direct-pointer.png">
<meta property="og:image" content="https://javaguide.cn/assets/90984624.e8c186ae.png">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/bg/desktop%E7%B1%BB%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84%E6%A6%82%E8%A7%88.png">
<meta property="og:image" content="g:\GitDocument\Study\images\%E9%9D%A2%E8%AF%95.assets\image-20230216161700631.png">
<meta property="og:image" content="https://oss.javaguide.cn/github/javaguide/java/jvm/class-loading-procedure.png">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/%E9%AA%8C%E8%AF%81%E9%98%B6%E6%AE%B5.png">
<meta property="og:image" content="https://oss.javaguide.cn/github/javaguide/java/jvm/symbol-reference-and-direct-reference.png">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/classloader_WPS%E5%9B%BE%E7%89%87.png">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/81f1813f371c40ffa1c1f6d78bc49ed9-new-image28314ec8-066f-451e-8373-4517917d6bf7.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/cs-basics/network/osi-7-model.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/cs-basics/network/osi-model-detail.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/cs-basics/network/tcp-ip-4-model.png">
<meta property="og:image" content="https://oss.javaguide.cn/github/javaguide/cs-basics/network/network-protocol-overview.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230224112955680.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230328105440470.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/cs-basics/network/tcp-shakes-hands-three-times.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/cs-basics/network/tcp-waves-four-times.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/cs-basics/network/tcp-send-window.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/cs-basics/network/tcp-receive-window.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/cs-basics/network/tcp-congestion-control.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230327185259706.png">
<meta property="og:image" content="https://s2.loli.net/2023/03/27/62nxJ4HAupo3Xl1.png">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/url%E8%BE%93%E5%85%A5%E5%88%B0%E5%B1%95%E7%A4%BA%E5%87%BA%E6%9D%A5%E7%9A%84%E8%BF%87%E7%A8%8B.jpg">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/%E7%8A%B6%E6%80%81%E7%A0%81.png">
<meta property="og:image" content="https://javaguide.cn/assets/HTTP1.0cache1.2f2b7eac.png">
<meta property="og:image" content="https://javaguide.cn/assets/HTTP1.0cache2.7430070e.png">
<meta property="og:image" content="https://gitee.com/xxd-blog/assets/HTTP1.1continue2.7d63532b.png">
<meta property="og:image" content="https://javaguide.cn/assets/arp_different_lan.ad156523.png">
<meta property="og:image" content="https://img-blog.csdnimg.cn/img_convert/735f55501e81898aa61b8032f7dbcb73.png">
<meta property="og:image" content="https://img-blog.csdnimg.cn/img_convert/b273efef5f2388e26414135672b00295.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/jvm/java-runtime-data-areas-jdk1.8.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230302151547639.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/L181kk2Eou-compress.jpg">
<meta property="og:image" content="https://javaguide.cn/assets/Linux%E6%9D%83%E9%99%90%E8%A7%A3%E8%AF%BB.7c1098a0.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230303102209654.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230303131632822.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230303131739253.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230303131825928.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230303132248344.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230303143512374.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230303144134674.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230303153409462.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230303153436831.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230303154302402.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230303183830783.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230303183855366.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230305184510960.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230305184719733.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230306143320709.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230306162816462.png">
<meta property="og:image" content="https://img-blog.csdnimg.cn/img_convert/d1794312b448516831369f869814ab39.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230306163736832.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/13526879-3037b144ed09eb88.png">
<meta property="og:image" content="https://img-blog.csdnimg.cn/20210420165326946.png">
<meta property="og:image" content="https://oss.javaguide.cn/github/javaguide/open-source-project/no-cluster-index.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/01.png">
<meta property="og:image" content="https://oss.javaguide.cn/github/javaguide/03.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/04-20220305234747840.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/04-20220305234956774.png">
<meta property="og:image" content="https://javaguide.cn/assets/trans_visible.048192c5.png">
<meta property="og:image" content="https://javaguide.cn/assets/c52ff79f-10e6-46cb-b5d4-3c9cbcc1934a.b60a6e78.png">
<meta property="og:image" content="https://javaguide.cn/assets/6a276e7a-b0da-4c7b-bdf7-c0c7b7b3b31c.2e496ea1.png">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/%E6%80%BB%E7%BB%93-%E5%B8%B8%E7%94%A8%E6%97%A5%E6%9C%9F%E5%AD%98%E5%82%A8%E6%96%B9%E5%BC%8F.jpg">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230308141312344.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230321140816128.png">
<meta property="og:image" content="https://img-blog.csdnimg.cn/20200108155524948.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,ow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTU3OTc4MA==,size_16,color_FFFFFF,t_70">
<meta property="og:image" content="https://img2020.cnblogs.com/blog/1552449/202109/1552449-20210930011640764-603796536.png">
<meta property="og:image" content="https://img-blog.csdn.net/20180824153416344?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L25ld2JpZV85MDc0ODY4NTI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70">
<meta property="og:image" content="https://upload-images.jianshu.io/upload_images/10007098-7c1a117c179541f4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1146/format/webp">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230308180003601.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/database/mongodb/replica-set-read-write-operations-primary.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/database/mongodb/sharded-cluster-production-architecture.png">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/%E7%89%A9%E7%90%86%E6%9C%BA%E5%9B%BE%E8%A7%A3.png">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%9B%BE%E8%A7%A3.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/image-20211110104003678.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/2e2b95eebf60b6d03f6c1476f4d7c697.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230308210737879.png">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/056c87751b9dd7b56f4264240fe96d00.png">
<meta property="og:image" content="https://img-blog.csdnimg.cn/img_convert/de6d2b213f112297298f3e223bf08f28.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230407142216990.png">
<meta property="og:image" content="https://s2.loli.net/2023/04/07/2aYzBoNJCfysLmQ.png">
<meta property="og:image" content="https://img-blog.csdnimg.cn/20190328163409289.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxODUwNDQ5,size_16,color_FFFFFF,t_70">
<meta property="og:image" content="https://img-blog.csdnimg.cn/20210309230152143.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDQ3MTQ5MA==,size_16,color_FFFFFF,t_70#pic_center">
<meta property="og:image" content="https://img-blog.csdnimg.cn/20210309230210999.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDQ3MTQ5MA==,size_16,color_FFFFFF,t_70#pic_center">
<meta property="og:image" content="https://img-blog.csdnimg.cn/20210309230233596.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDQ3MTQ5MA==,size_16,color_FFFFFF,t_70#pic_center">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230407183200324.png">
<meta property="og:image" content="https://s2.loli.net/2023/04/07/MPzUIqOEK3h7pVR.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230407193955779.png">
<meta property="og:image" content="https://mmbiz.qpic.cn/mmbiz_png/OqTAl3WTC7EicibZpPpiaSHI238uMfVZibJLfzUefrlUS9Tv4o2JogzQWtDeMbFmTU5NuH4VCVqsuW54HSYRTDYNZg/640?wx_fmt=gif&wxfrom=5&wx_lazy=1&wx_co=1">
<meta property="og:image" content="https://mmbiz.qpic.cn/mmbiz_png/OqTAl3WTC7Ejn0oXWsb4JZTOgOH40F5mDochiaylR5w9fiaFLY3TBcxibP36Xp4RKRyyXuYWlEgpuibpkItcUia4Eiag/640?wx_fmt=gif&wxfrom=5&wx_lazy=1&wx_co=1">
<meta property="og:image" content="https://ask.qcloudimg.com/http-save/yehe-5086501/ns0djumg0i.png?imageView2/2/w/2560/h/7000">
<meta property="og:image" content="https://ask.qcloudimg.com/http-save/yehe-5086501/mcvawjt154.png?imageView2/2/w/2560/h/7000">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/system-design/security/20210615151716340.png">
<meta property="og:image" content="https://s2.loli.net/2023/03/26/Bh3SlqiVzMKJoCn.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230326204712067.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230327105553928.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230327132042657.png">
<meta property="og:image" content="https://oscimg.oschina.net/oscnet/up-890fa3212e8bf72886a595a34654918486c.png">
<meta property="og:image" content="https://javaguide.cn/assets/jvm-local-lock.31b27bf9.png">
<meta property="og:image" content="https://javaguide.cn/assets/distributed-lock.924e7216.png">
<meta property="og:image" content="https://javaguide.cn/assets/distributed-lock-setnx.6f86bb0f.png">
<meta property="og:image" content="https://javaguide.cn/assets/distributed-lock-redisson-renew-expiration.878c6f2a.png">
<meta property="og:image" content="https://img-blog.csdnimg.cn/20201119155758770.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM1ODQzMDk1,size_16,color_FFFFFF,t_70#pic_center">
<meta property="og:image" content="https://javaguide.cn/assets/redis-master-slave-distributed-lock.ccc5be73.png">
<meta property="og:image" content="https://javaguide.cn/assets/distributed-lock-zookeeper.f476fb11.png">
<meta property="og:image" content="https://oss.javaguide.cn/github/javaguide/distributed-system/zookeeper/zookeeper-watcher.png">
<meta property="og:image" content="https://oss.javaguide.cn/github/javaguide/distributed-system/zookeeper/zookeeper-cluster.png">
<meta property="og:image" content="https://oss.javaguide.cn/github/javaguide/distributed-system/zookeeper/zookeeper-cluser-roles.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230330134810413.png">
<meta property="og:image" content="https://s2.loli.net/2023/03/30/nXPCF2jdA85gZfG.png">
<meta property="og:image" content="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/80854635d48c42d896dbaa066abf5c26~tplv-k3u1fbpfcp-zoom-1.image">
<meta property="og:image" content="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b64c7f25a5d24766889da14260005e31~tplv-k3u1fbpfcp-zoom-1.image">
<meta property="og:image" content="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/78816271d3ab52424bfd5ad3086c1a0f.png">
<meta property="og:image" content="https://s2.loli.net/2023/04/10/pAoiQPOj4SE2Mev.png">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/Asynchronous-message-queue.png">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/%E5%89%8A%E5%B3%B0-%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97.png">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97-%E8%A7%A3%E8%80%A6.png">
<meta property="og:image" content="https://javaguide.cn/assets/message-queue-pub-sub-model.63a717b4.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230402195154372.png">
<meta property="og:image" content="https://s2.loli.net/2023/04/02/UyIczG7FtWlea9k.png">
<meta property="og:image" content="https://javaguide.cn/assets/message-queue-queue-model.3aa809bf.png">
<meta property="og:image" content="https://javaguide.cn/assets/message-queue-pub-sub-model.63a717b4.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230311155545137.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230408140230927.png">
<meta property="og:image" content="https://s2.loli.net/2023/04/08/qRGUA26MFovcytE.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\image-20230408141302450.png">
<meta property="og:image" content="https://s2.loli.net/2023/04/08/nWqGTubrwzF9xg8.png">
<meta property="og:image" content="https://s2.loli.net/2023/04/08/4dZ23ukBEoPeU7i.png">
<meta property="og:image" content="https://oss.javaguide.cn/github/javaguide/high-performance/message-queue20210507200944439.png">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\%E9%9D%A2%E8%AF%95%E9%97%AE%E9%A2%98.png">
<meta property="og:image" content="https://s2.loli.net/2023/04/08/fq7YEDJbCikROlc.png">
<meta property="og:image" content="https://s2.loli.net/2023/04/10/3pshYuMU8Qtc9qb.png">
<meta property="og:image" content="https://s2.loli.net/2023/04/08/waMbRjI6q4TDKEY.png">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef3857fefaa079.jpg">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef38687488a5a4.jpg">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef386fa3be1e53.jpg">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef38798d7a987f.png">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef387fba311cda.jpg">
<meta property="og:image" content="e:\XxdBlog\source_posts\images\test\RabbitMQ.png">
<meta property="og:image" content="https://oss.javaguide.cn/github/javaguide/rabbitmq/96388546.jpg">
<meta property="og:image" content="https://oss.javaguide.cn/github/javaguide/rabbitmq/67952922.jpg">
<meta property="og:image" content="https://oss.javaguide.cn/github/javaguide/rabbitmq/37008021.jpg">
<meta property="og:image" content="https://oss.javaguide.cn/github/javaguide/rabbitmq/73843.jpg">
<meta property="og:image" content="https://static001.infoq.cn/resource/image/75/03/75938d1010138ce66e38c6ed0392f103.png">
<meta property="og:image" content="https://static001.infoq.cn/resource/image/ec/93/eca0e5eaa35dac938c673fecf2ec9a93.png">
<meta property="og:image" content="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/%E7%BD%91%E7%AB%99%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95.png">
<meta property="article:published_time" content="2023-04-11T01:01:10.000Z">
<meta property="article:modified_time" content="2023-04-11T01:16:46.213Z">
<meta property="article:author" content="John Doe">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="g:\GitDocument\Study\%E9%9D%A2%E8%AF%95.assets\image-20230105193531104.png">
  
    <link rel="alternate" href="/xxd-blog/atom.xml" title="Hexo" type="application/atom+xml">
  
  
    <link rel="shortcut icon" href="/xxd-blog/favicon.png">
  
  
    
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/typeface-source-code-pro@0.0.71/index.min.css">

  
  
<link rel="stylesheet" href="/xxd-blog/css/style.css">

  
    
<link rel="stylesheet" href="/xxd-blog/fancybox/jquery.fancybox.min.css">

  
<meta name="generator" content="Hexo 6.3.0"></head>

<body>
  <div id="container">
    <div id="wrap">
      <header id="header">
  <div id="banner"></div>
  <div id="header-outer" class="outer">
    <div id="header-title" class="inner">
      <h1 id="logo-wrap">
        <a href="/xxd-blog/" id="logo">Hexo</a>
      </h1>
      
    </div>
    <div id="header-inner" class="inner">
      <nav id="main-nav">
        <a id="main-nav-toggle" class="nav-icon"></a>
        
          <a class="main-nav-link" href="/xxd-blog/">Home</a>
        
          <a class="main-nav-link" href="/xxd-blog/archives">Archives</a>
        
      </nav>
      <nav id="sub-nav">
        
          <a id="nav-rss-link" class="nav-icon" href="/xxd-blog/atom.xml" title="RSS Feed"></a>
        
        <a id="nav-search-btn" class="nav-icon" title="Search"></a>
      </nav>
      <div id="search-form-wrap">
        <form action="//google.com/search" method="get" accept-charset="UTF-8" class="search-form"><input type="search" name="q" class="search-form-input" placeholder="Search"><button type="submit" class="search-form-submit">&#xF002;</button><input type="hidden" name="sitesearch" value="https://gitee.com/xxditem"></form>
      </div>
    </div>
  </div>
</header>

      <div class="outer">
        <section id="main"><article id="post-面试" class="h-entry article article-type-post" itemprop="blogPost" itemscope itemtype="https://schema.org/BlogPosting">
  <div class="article-meta">
    <a href="/xxd-blog/2023/04/11/%E9%9D%A2%E8%AF%95/" class="article-date">
  <time class="dt-published" datetime="2023-04-11T01:01:10.000Z" itemprop="datePublished">2023-04-11</time>
</a>
    
  </div>
  <div class="article-inner">
    
    
      <header class="article-header">
        
  
    <h1 class="p-name article-title" itemprop="headline name">
      test
    </h1>
  

      </header>
    
    <div class="e-content article-entry" itemprop="articleBody">
      
        <p>[toc]</p>
<h1 id="JAVA"><a href="#JAVA" class="headerlink" title="JAVA"></a>JAVA</h1><h2 id="JAVA基础上"><a href="#JAVA基础上" class="headerlink" title="JAVA基础上"></a>JAVA基础上</h2><h3 id="成员变量与局部变量的区别？"><a href="#成员变量与局部变量的区别？" class="headerlink" title="成员变量与局部变量的区别？"></a>成员变量与局部变量的区别？</h3><ul>
<li><strong>语法形式</strong> ：从语法形式上看，成员变量是属于类的，而局部变量是在代码块或方法中定义的变量或是方法的参数；成员变量可以被 <code>public</code>,<code>private</code>,<code>static</code> 等修饰符所修饰，而局部变量不能被访问控制修饰符及 <code>static</code> 所修饰；但是，成员变量和局部变量都能被 <code>final</code> 所修饰。</li>
<li><strong>存储方式</strong> ：从变量在内存中的存储方式来看,如果成员变量是使用 <code>static</code> 修饰的，那么这个成员变量是属于类的，如果没有使用 <code>static</code> 修饰，这个成员变量是属于实例的。而对象存在于堆内存，局部变量则存在于栈内存。</li>
<li><strong>生存时间</strong> ：从变量在内存中的生存时间上看，成员变量是对象的一部分，它随着对象的创建而存在，而局部变量随着方法的调用而自动生成，随着方法的调用结束而消亡。</li>
<li><strong>默认值</strong> ：从变量是否有默认值来看，成员变量如果没有被赋初始值，则会自动以类型的默认值而赋值（一种情况例外:被 <code>final</code> 修饰的成员变量也必须显式地赋值），而局部变量则不会自动赋值</li>
</ul>
<hr>
<h3 id="字符型常量和字符串常量的区别"><a href="#字符型常量和字符串常量的区别" class="headerlink" title="字符型常量和字符串常量的区别?"></a>字符型常量和字符串常量的区别?</h3><ol>
<li><strong>形式</strong> : 字符常量是单引号引起的一个字符，字符串常量是双引号引起的 0 个或若干个字符。</li>
<li><strong>含义</strong> : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。</li>
<li><strong>占内存大小</strong> ： 字符常量只占 2 个字节; 字符串常量占若干个字节。</li>
</ol>
<p>(<strong>注意： <code>char</code> 在 Java 中占两个字节</strong>)</p>
<hr>
<h3 id="静态方法为什么不能调用非静态成员"><a href="#静态方法为什么不能调用非静态成员" class="headerlink" title="静态方法为什么不能调用非静态成员?"></a>静态方法为什么不能调用非静态成员?</h3><p>这个需要结合 JVM 的相关知识，主要原因如下：</p>
<ol>
<li>静态方法是属于类的，在类加载的时候就会分配内存，可以通过类名直接访问。而非静态成员属于实例对象，只有在对象实例化之后才存在，需要通过类的实例对象去访问。</li>
<li>在类的非静态成员不存在的时候静态成员就已经存在了，此时调用在内存中还不存在的非静态成员，属于非法操作。</li>
</ol>
<hr>
<h3 id="重载和重写有什么区别？"><a href="#重载和重写有什么区别？" class="headerlink" title="重载和重写有什么区别？"></a>重载和重写有什么区别？</h3><blockquote>
<p>重载就是同样的一个方法能够根据输入数据的不同，做出不同的处理</p>
<p>重写就是当子类继承自父类的相同方法，输入数据一样，但要做出有别于父类的响应时，你就要覆盖父类方法</p>
</blockquote>
<p><strong>重载</strong></p>
<p>发生在同一个类中（或者父类和子类之间），方法名必须相同，参数类型不同、个数不同、顺序不同，方法返回值和访问修饰符可以不同。</p>
<p>综上：重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。</p>
<p><strong>重写</strong></p>
<p>重写发生在运行期，是子类对父类的允许访问的方法的实现过程进行重新编写。</p>
<ol>
<li>方法名、参数列表必须相同，子类方法返回值类型应比父类方法返回值类型更小或相等，抛出的异常范围小于等于父类，访问修饰符范围大于等于父类。</li>
<li>如果父类方法访问修饰符为 <code>private/final/static</code> 则子类就不能重写该方法，但是被 <code>static</code> 修饰的方法能够被再次声明。</li>
<li>构造方法无法被重写</li>
</ol>
<p>综上：<strong>重写就是子类对父类方法的重新改造，外部样子不能改变，内部逻辑可以改变。</strong></p>
<p><img src="G:\GitDocument\Study\面试.assets\image-20230105193531104.png" alt="image-20230105193531104"></p>
<p><strong>方法的重写要遵循“两同两小一大”</strong>：</p>
<ul>
<li>“两同”即方法名相同、形参列表相同；</li>
<li>“两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等，子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等；</li>
<li>“一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。</li>
</ul>
<hr>
<h3 id="包装类型的缓存机制"><a href="#包装类型的缓存机制" class="headerlink" title="包装类型的缓存机制"></a>包装类型的缓存机制</h3><p>Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。</p>
<p><code>Byte</code>,<code>Short</code>,<code>Integer</code>,<code>Long</code> 这 4 种包装类默认创建了数值 <strong>[-128，127]</strong> 的相应类型的缓存数据，<code>Character</code> 创建了数值在 <strong>[0,127]</strong> 范围的缓存数据，<code>Boolean</code> 直接返回 <code>True</code> or <code>False</code>。</p>
<p><strong>Integer 缓存源码：</strong></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">public static Integer valueOf(int i) &#123;</span><br><span class="line">    if (i &gt;= IntegerCache.low &amp;&amp; i &lt;= IntegerCache.high)</span><br><span class="line">        return IntegerCache.cache[i + (-IntegerCache.low)];</span><br><span class="line">    return new Integer(i);</span><br><span class="line">&#125;</span><br><span class="line">private static class IntegerCache &#123;</span><br><span class="line">    static final int low = -128;</span><br><span class="line">    static final int high;</span><br><span class="line">    static &#123;</span><br><span class="line">        // high value may be configured by property</span><br><span class="line">        int h = 127;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>下面的代码的输出结果是 <code>true</code> 还是 <code>false</code> 呢？</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Integer i1 = 40;</span><br><span class="line">Integer i2 = new Integer(40);</span><br><span class="line">System.out.println(i1==i2);</span><br></pre></td></tr></table></figure>

<p><code>Integer i1=40</code> 这一行代码会发生装箱，也就是说这行代码等价于 <code>Integer i1=Integer.valueOf(40)</code> 。因此，<code>i1</code> 直接使用的是缓存中的对象。而<code>Integer i2 = new Integer(40)</code> 会直接创建新的对象。</p>
<p>因此，答案是 <code>false</code> 。</p>
<p>记住：<strong>所有整型包装类对象之间值的比较，全部使用 equals 方法比较</strong>。</p>
<hr>
<h3 id="自动装箱与拆箱"><a href="#自动装箱与拆箱" class="headerlink" title="自动装箱与拆箱"></a>自动装箱与拆箱</h3><p><strong>什么是自动拆装箱？</strong></p>
<ul>
<li><strong>装箱</strong>：将基本类型用它们对应的引用类型包装起来；</li>
<li><strong>拆箱</strong>：将包装类型转换为基本数据类型；</li>
</ul>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Integer i = 10;  //装箱</span><br><span class="line">int n = i;   //拆箱</span><br></pre></td></tr></table></figure>

<ul>
<li><code>Integer i = 10</code> 等价于 <code>Integer i = Integer.valueOf(10)</code></li>
<li><code>int n = i</code> 等价于 <code>int n = i.intValue()</code>;</li>
</ul>
<p>注意：<strong>如果频繁拆装箱的话，也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。</strong></p>
<hr>
<h3 id="为什么浮点数运算的时候会有精度丢失的风险？"><a href="#为什么浮点数运算的时候会有精度丢失的风险？" class="headerlink" title="为什么浮点数运算的时候会有精度丢失的风险？"></a>为什么浮点数运算的时候会有精度丢失的风险？</h3><p>这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的，而且计算机在表示一个数字时，宽度是有限的，无限循环的小数存储在计算机时，只能被截断，所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。</p>
<h3 id="如何解决浮点数运算的精度丢失问题？"><a href="#如何解决浮点数运算的精度丢失问题？" class="headerlink" title="如何解决浮点数运算的精度丢失问题？"></a>如何解决浮点数运算的精度丢失问题？</h3><p><code>BigDecimal</code> 可以实现对浮点数的运算，不会造成精度丢失。通常情况下，大部分需要浮点数精确运算结果的业务场景（比如涉及到钱的场景）都是通过 <code>BigDecimal</code> 来做的。</p>
<hr>
<h3 id="超过-long-整型的数据应该如何表示？"><a href="#超过-long-整型的数据应该如何表示？" class="headerlink" title="超过 long 整型的数据应该如何表示？"></a>超过 long 整型的数据应该如何表示？</h3><p>在 Java 中，64 位 long 整型是最大的整数类型。</p>
<p><code>BigInteger</code> 内部使用 <code>int[]</code> 数组来存储任意大小的整形数据。</p>
<p>相对于常规整数类型的运算来说，<code>BigInteger</code> 运算的效率会相对较低。</p>
<hr>
<h2 id="JAVA基础中"><a href="#JAVA基础中" class="headerlink" title="JAVA基础中"></a>JAVA基础中</h2><h3 id="面向对象基础"><a href="#面向对象基础" class="headerlink" title="面向对象基础"></a>面向对象基础</h3><hr>
<h4 id="对象"><a href="#对象" class="headerlink" title="对象"></a>对象</h4><ul>
<li><p>对象实例（创建对象）</p>
<p>new对象，对象实例在堆中</p>
<ul>
<li><p>对象实例相等</p>
<p>内存中存放的内容相等</p>
</li>
</ul>
</li>
<li><p>对象引用</p>
<p>对象引用存放在栈内存中，指向对象实例</p>
<ul>
<li><p>引用相等</p>
<p>指向的内存地址相同</p>
</li>
</ul>
</li>
<li><p>构造方法</p>
</li>
</ul>
<hr>
<h4 id="面向对象与面向过程的区别"><a href="#面向对象与面向过程的区别" class="headerlink" title="面向对象与面向过程的区别"></a>面向对象与面向过程的区别</h4><p>解决问题的方式不同</p>
<ul>
<li>面向对象：先抽象出对象，通过用对象执行方法解决问题</li>
<li>面向过程：把解决问题的过程拆成一个个方法，通过方法的执行解决问题</li>
</ul>
<h5 id="为什么面向过程性能更高"><a href="#为什么面向过程性能更高" class="headerlink" title="为什么面向过程性能更高"></a><strong>为什么面向过程性能更高</strong></h5><ol>
<li>面向对象在类调用时需要实例化对象，消耗资源</li>
<li>Java是半编译语言，最终执行的代码不是可以直接被CPU执行的二进制机械码</li>
</ol>
<hr>
<h4 id="如果一个类没有声明构造方法，该程序能正确执行吗"><a href="#如果一个类没有声明构造方法，该程序能正确执行吗" class="headerlink" title="如果一个类没有声明构造方法，该程序能正确执行吗?"></a>如果一个类没有声明构造方法，该程序能正确执行吗?</h4><p>如果一个类没有声明构造方法，也可以执行！因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法（无论是否有参），Java 就不会再添加默认的无参数的构造方法了，我们一直在不知不觉地使用构造方法，这也是为什么我们在创建对象的时候后面要加一个括号（因为要调用无参的构造方法）。如果我们重载了有参的构造方法，记得都要把无参的构造方法也写出来（无论是否用到），因为这可以帮助我们在创建对象的时候少踩坑</p>
<hr>
<h4 id="面向对象三大特征"><a href="#面向对象三大特征" class="headerlink" title="面向对象三大特征"></a>面向对象三大特征</h4><h5 id="封装"><a href="#封装" class="headerlink" title="封装"></a>封装</h5><p>封装是指把一个对象的状态信息（也就是属性）隐藏在对象内部，不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。</p>
<h5 id="继承"><a href="#继承" class="headerlink" title="继承"></a>继承</h5><p>继承是使用已存在的类的定义作为基础建立新类的技术，新类的定义可以增加新的数据或新的功能，也可以用父类的功能，但不能选择性地继承父类。通过使用继承，可以快速地创建新的类，可以提高代码的重用，程序的可维护性，节省大量创建新类的时间 ，提高我们的开发效率。</p>
<p><strong>关于继承如下 3 点请记住：</strong></p>
<ol>
<li>子类拥有父类对象所有的属性和方法（包括私有属性和私有方法），但是父类中的私有属性和方法子类是无法访问，<strong>只是拥有</strong>。</li>
<li>子类可以拥有自己属性和方法，即子类可以对父类进行扩展。</li>
<li>子类可以用自己的方式实现父类的方法。（以后介绍）。</li>
</ol>
<h5 id="x3D-x3D-多态-x3D-x3D"><a href="#x3D-x3D-多态-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;多态&#x3D;&#x3D;"></a>&#x3D;&#x3D;多态&#x3D;&#x3D;</h5><p><a target="_blank" rel="noopener" href="https://www.cnblogs.com/taiziyenezha/p/15135100.html#:~:text=%E5%A4%9A%E6%80%81%E5%B0%B1%E6%98%AF%E6%8C%87%E7%A8%8B%E5%BA%8F%E4%B8%AD,%E8%A1%8C%E6%9C%9F%E9%97%B4%E6%89%8D%E8%83%BD%E5%86%B3%E5%AE%9A%E3%80%82">https://www.cnblogs.com/taiziyenezha/p/15135100.html#:~:text=%E5%A4%9A%E6%80%81%E5%B0%B1%E6%98%AF%E6%8C%87%E7%A8%8B%E5%BA%8F%E4%B8%AD,%E8%A1%8C%E6%9C%9F%E9%97%B4%E6%89%8D%E8%83%BD%E5%86%B3%E5%AE%9A%E3%80%82</a></p>
<p>多态，顾名思义，表示一个对象具有多种的状态，具体表现为父类的引用指向子类的实例。</p>
<p><strong>多态的特点:</strong></p>
<ul>
<li>对象类型和引用类型之间具有继承（类）&#x2F;实现（接口）的关系；</li>
<li>引用类型变量发出的方法调用的到底是哪个类中的方法，必须在程序运行期间才能确定；</li>
<li>多态不能调用“只在子类存在但在父类不存在”的方法；</li>
<li>如果子类重写了父类的方法，真正执行的是子类覆盖的方法，如果子类没有覆盖父类的方法，执行的是父类的方法。</li>
</ul>
<p>好处：</p>
<ul>
<li>实现了实现类的自动切换</li>
</ul>
<p>坏处：</p>
<ul>
<li>多态写法无法访问子类独有的功能</li>
</ul>
<h6 id="引用类型转换"><a href="#引用类型转换" class="headerlink" title="引用类型转换"></a>引用类型转换</h6><p><font color='orange'><strong>编译看左边，运行看右边</strong></font></p>
<ul>
<li>向上转型（自动转型）<ul>
<li>子-&gt;父</li>
</ul>
</li>
<li>向下转型（强制转型）<ul>
<li>父-&gt;子</li>
</ul>
</li>
</ul>
<p>通过 &#x3D;&#x3D;<code>变量名 instanceof 数据类型</code>&#x3D;&#x3D; 判断当前变量是否是该类型，是则可以强转然后调用该类型的方法</p>
<hr>
<h4 id="接口和抽象类有什么共同点和区别？"><a href="#接口和抽象类有什么共同点和区别？" class="headerlink" title="接口和抽象类有什么共同点和区别？"></a>接口和抽象类有什么共同点和区别？</h4><p><a target="_blank" rel="noopener" href="https://www.runoob.com/java/java-interfaces.html">https://www.runoob.com/java/java-interfaces.html</a></p>
<p><strong>共同点</strong> ：</p>
<ul>
<li>都不能被实例化。</li>
<li>都可以包含抽象方法。</li>
<li>都可以有默认实现的方法（Java 8 可以用 <code>default</code> 关键字在接口中定义默认方法）。</li>
</ul>
<p><strong>区别</strong> ：</p>
<ul>
<li>接口主要用于对类的行为进行约束，你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用，强调的是所属关系。</li>
<li>一个类只能继承一个类，但是可以实现多个接口。</li>
<li>接口中的成员变量只能是 <code>public static final</code> 类型的，不能被修改且必须有初始值，而抽象类的成员变量默认 default，可在子类中被重新定义，也可被重新赋值。</li>
</ul>
<hr>
<h4 id="深拷贝、浅拷贝、引用拷贝"><a href="#深拷贝、浅拷贝、引用拷贝" class="headerlink" title="深拷贝、浅拷贝、引用拷贝"></a>深拷贝、浅拷贝、<strong>引用拷贝</strong></h4><ul>
<li><strong>浅拷贝</strong>：浅拷贝会在堆上创建一个新的对象（区别于引用拷贝的一点），不过，如果原对象内部的属性是引用类型的话，浅拷贝会直接复制内部对象的引用地址，也就是说拷贝对象和原对象共用同一个内部对象。</li>
<li><strong>深拷贝</strong> ：深拷贝会完全复制整个对象，包括这个对象所包含的内部对象。</li>
</ul>
<p><strong>那什么是引用拷贝呢？</strong> 简单来说，引用拷贝就是两个不同的引用指向同一个对象。</p>
<p><img src="https://oss.javaguide.cn/github/javaguide/java/basis/shallow&deep-copy.png" alt="浅拷贝、深拷贝、引用拷贝示意图"></p>
<hr>
<h3 id="Java常见类"><a href="#Java常见类" class="headerlink" title="Java常见类"></a>Java常见类</h3><h4 id="Object"><a href="#Object" class="headerlink" title="Object"></a>Object</h4><h5 id="x3D-x3D-和-equals-的区别"><a href="#x3D-x3D-和-equals-的区别" class="headerlink" title="&#x3D;&#x3D; 和 equals() 的区别"></a>&#x3D;&#x3D; 和 equals() 的区别</h5><ul>
<li>对于基本数据类型来说，<code>==</code> 比较的是值。</li>
<li>对于引用数据类型来说，<code>==</code> 比较的是对象的内存地址。</li>
</ul>
<blockquote>
<p>因为 Java 只有值传递，所以，对于 &#x3D;&#x3D; 来说，不管是比较基本数据类型，还是引用数据类型的变量，其本质比较的都是值，只是引用类型变量存的值是对象的地址。</p>
</blockquote>
<h5 id="为什么要有-hashCode？"><a href="#为什么要有-hashCode？" class="headerlink" title="为什么要有 hashCode？"></a>为什么要有 hashCode？</h5><p>当你把对象加入 <code>HashSet</code> 时，<code>HashSet</code> 会先计算对象的 <code>hashCode</code> 值来判断对象加入的位置，同时也会与其他已经加入的对象的 <code>hashCode</code> 值作比较，如果没有相符的 <code>hashCode</code>，<code>HashSet</code> 会假设对象没有重复出现。但是如果发现有相同 <code>hashCode</code> 值的对象，这时会调用 <code>equals()</code> 方法来检查 <code>hashCode</code> 相等的对象是否真的相同。如果两者相同，<code>HashSet</code> 就不会让其加入操作成功。如果不同的话，就会重新散列到其他位置。这样我们就大大减少了 <code>equals</code> 的次数，相应就大大提高了执行速度。</p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230406183830873.png" alt="image-20230406183830873"></p>
<p><img src="https://s2.loli.net/2023/04/06/cgJWzxslyOhSQ8P.png"></p>
<h5 id="那为什么-JDK-还要同时提供这两个方法呢？"><a href="#那为什么-JDK-还要同时提供这两个方法呢？" class="headerlink" title="那为什么 JDK 还要同时提供这两个方法呢？"></a><strong>那为什么 JDK 还要同时提供这两个方法呢？</strong></h5><p>同样的 <code>hashCode</code> 有多个对象，它会继续使用 <code>equals()</code> 来判断是否真的相同。也就是说 <code>hashCode</code> 帮助我们大大缩小了查找成本。</p>
<h5 id="那为什么不只提供-hashCode-方法呢？"><a href="#那为什么不只提供-hashCode-方法呢？" class="headerlink" title="那为什么不只提供 hashCode() 方法呢？"></a><strong>那为什么不只提供 <code>hashCode()</code> 方法呢？</strong></h5><p>这是因为两个对象的<code>hashCode</code> 值相等并不代表两个对象就相等。</p>
<h5 id="那为什么两个对象有相同的-hashCode-值，它们也不一定是相等的？"><a href="#那为什么两个对象有相同的-hashCode-值，它们也不一定是相等的？" class="headerlink" title="那为什么两个对象有相同的 hashCode 值，它们也不一定是相等的？"></a><strong>那为什么两个对象有相同的 <code>hashCode</code> 值，它们也不一定是相等的？</strong></h5><p>因为 <code>hashCode()</code> 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞，但这也与数据值域分布的特性有关（所谓哈希碰撞也就是指的是不同的对象得到相同的 <code>hashCode</code> )。</p>
<h5 id="为什么重写-equals-时必须重写-hashCode-方法？（Studen-id与name做案例）"><a href="#为什么重写-equals-时必须重写-hashCode-方法？（Studen-id与name做案例）" class="headerlink" title="为什么重写 equals() 时必须重写 hashCode() 方法？（Studen id与name做案例）"></a>为什么重写 equals() 时必须重写 hashCode() 方法？（Studen id与name做案例）</h5><p>因为两个相等的对象的 <code>hashCode</code> 值必须是相等。也就是说如果 <code>equals</code> 方法判断两个对象是相等的，那这两个对象的 <code>hashCode</code> 值也要相等，若不重写hashCode，则hashCode不等，equal相等，与equal相等hashCode也相等矛盾。</p>
<p>如果重写 <code>equals()</code> 时没有重写 <code>hashCode()</code> 方法的话就可能会导致 <code>equals</code> 方法判断是相等的两个对象，<code>hashCode</code> 值却不相等。</p>
<p><strong>思考</strong> ：重写 <code>equals()</code> 时没有重写 <code>hashCode()</code> 方法的话，使用 <code>HashMap</code> 可能会出现什么问题。</p>
<p><strong>重写了equals方法，不重写hashCode方法时，可能会出现equals方法返回为true，而hashCode方法却返回不同的结果。</strong></p>
<hr>
<h4 id="String"><a href="#String" class="headerlink" title="String"></a>String</h4><h5 id="String、StringBuffer、StringBuilder-的区别？"><a href="#String、StringBuffer、StringBuilder-的区别？" class="headerlink" title="String、StringBuffer、StringBuilder 的区别？"></a>String、StringBuffer、StringBuilder 的区别？</h5><ul>
<li>可变性</li>
<li>线程安全性</li>
<li>性能</li>
</ul>
<h5 id="String-为什么是不可变的"><a href="#String-为什么是不可变的" class="headerlink" title="String 为什么是不可变的?"></a>String 为什么是不可变的?</h5><ul>
<li>保存字符串的数组被 <code>final</code> 修饰且为私有的，并且<code>String</code> 类没有提供&#x2F;暴露修改这个字符串的方法。（字符数组无法被修改）</li>
<li><code>String</code> 类被 <code>final</code> 修饰导致其不能被继承，进而避免了子类破坏 <code>String</code> 不可变。</li>
</ul>
<h5 id="Java-9-为何要将-String-的底层实现由-char-改成了-byte"><a href="#Java-9-为何要将-String-的底层实现由-char-改成了-byte" class="headerlink" title="Java 9 为何要将 String 的底层实现由 char[] 改成了 byte[] ?"></a><strong>Java 9 为何要将 <code>String</code> 的底层实现由 <code>char[]</code> 改成了 <code>byte[]</code> ?</strong></h5><p>新版的 String 其实支持两个编码方案： Latin-1 和 UTF-16。Latin-1 编码方案下，<code>byte</code> 占一个字节(8 位)，<code>char</code> 占用 2 个字节（16），<code>byte</code> 相较 <code>char</code> 节省一半的内存空间。</p>
<h5 id="字符串拼接用“-”-还是-StringBuilder"><a href="#字符串拼接用“-”-还是-StringBuilder" class="headerlink" title="字符串拼接用“+” 还是 StringBuilder?"></a>字符串拼接用“+” 还是 StringBuilder?</h5><p>实际上是通过 <code>StringBuilder</code> 调用 <code>append()</code> 方法实现的，拼接完成之后调用 <code>toString()</code> 得到一个 <code>String</code> 对象 。</p>
<p>不过，在循环内使用“+”进行字符串的拼接的话，存在比较明显的缺陷：<strong>编译器不会创建单个 <code>StringBuilder</code> 以复用，会导致创建过多的 <code>StringBuilder</code> 对象</strong>。</p>
<h5 id="字符串常量池的作用了解吗？"><a href="#字符串常量池的作用了解吗？" class="headerlink" title="字符串常量池的作用了解吗？"></a>字符串常量池的作用了解吗？</h5><p><strong>字符串常量池</strong> 是 JVM 为了提升性能和减少内存消耗针对字符串（String 类）专门开辟的一块区域，主要目的是为了避免字符串的重复创建。</p>
<h5 id="String-s1-x3D-new-String-“abc”-这句话创建了几个字符串对象？"><a href="#String-s1-x3D-new-String-“abc”-这句话创建了几个字符串对象？" class="headerlink" title="String s1 &#x3D; new String(“abc”);这句话创建了几个字符串对象？"></a>String s1 &#x3D; new String(“abc”);这句话创建了几个字符串对象？</h5><p>会创建 1 或 2 个字符串对象。</p>
<h5 id="intern-方法有什么作用"><a href="#intern-方法有什么作用" class="headerlink" title="intern 方法有什么作用?"></a>intern 方法有什么作用?</h5><p>将指定的字符串对象的引用保存在&#x3D;&#x3D;字符串常量池&#x3D;&#x3D;中</p>
<ul>
<li>如果&#x3D;&#x3D;字符串常量池&#x3D;&#x3D;中保存了对应的字符串&#x3D;&#x3D;对象的引用&#x3D;&#x3D;，就直接返回该引用。</li>
<li>如果字符串常量池中没有保存了对应的字符串对象的引用，那就在常量池中创建一个指向该字符串对象的引用并返回。（常量池中创建该对象，并返回其引用）</li>
</ul>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">// 在堆中创建字符串对象”Java“</span><br><span class="line">// 将字符串对象”Java“的引用保存在字符串常量池中</span><br><span class="line">String s1 = &quot;Java&quot;;</span><br><span class="line">// 直接返回字符串常量池中字符串对象”Java“对应的引用</span><br><span class="line">String s2 = s1.intern();</span><br><span class="line">// 会在堆中在单独创建一个字符串对象,new的String并不会保存到字符串常量池中，s3被赋值为java，故s4得到的是java在常量池中的引用</span><br><span class="line">String s3 = new String(&quot;Java&quot;);</span><br><span class="line">// 直接返回字符串常量池中字符串对象”Java“对应的引用</span><br><span class="line">String s4 = s3.intern();</span><br><span class="line">// s1 和 s2 指向的是堆中的同一个对象</span><br><span class="line">System.out.println(s1 == s2); // true</span><br><span class="line">// s3 和 s4 指向的是堆中不同的对象</span><br><span class="line">System.out.println(s3 == s4); // false</span><br><span class="line">// s1 和 s4 指向的是堆中的同一个对象</span><br><span class="line">System.out.println(s1 == s4); //true</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h5 id="x3D-x3D-String-类型的变量和常量做“-”运算时发生了什么？-x3D-x3D"><a href="#x3D-x3D-String-类型的变量和常量做“-”运算时发生了什么？-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;String 类型的变量和常量做“+”运算时发生了什么？&#x3D;&#x3D;"></a>&#x3D;&#x3D;String 类型的变量和常量做“+”运算时发生了什么？&#x3D;&#x3D;</h5><p>&#x3D;&#x3D;<strong>常量折叠</strong>&#x3D;&#x3D;</p>
<p><strong>对于编译期可以确定值的字符串，也就是常量字符串 ，jvm 会将其存入字符串常量池。并且，字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池，这个得益于编译器的优化。</strong></p>
<p><strong>引用的值在程序编译期是无法确定的，编译器无法对其进行优化。</strong></p>
<p>&#x3D;&#x3D;常量&#x3D;&#x3D;</p>
<ul>
<li>基本数据类型( <code>byte</code>、<code>boolean</code>、<code>short</code>、<code>char</code>、<code>int</code>、<code>float</code>、<code>long</code>、<code>double</code>)以及<strong>字符串常量</strong>（final修饰在编译期确定值）。</li>
<li><code>final</code> 修饰的基本数据类型和字符串变量</li>
<li>字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算（加减乘除）、基本数据类型的位运算（&lt;&lt;、&gt;&gt;、&gt;&gt;&gt; ）</li>
</ul>
<hr>
<hr>
<h2 id="JAVA基础下"><a href="#JAVA基础下" class="headerlink" title="JAVA基础下"></a>JAVA基础下</h2><h3 id="try-catch-finally-如何使用？"><a href="#try-catch-finally-如何使用？" class="headerlink" title="try-catch-finally 如何使用？"></a>try-catch-finally 如何使用？</h3><ul>
<li><code>finally</code> 块 ： 无论是否捕获或处理异常，<code>finally</code> 块里的语句都会被执行。当在 <code>try</code> 块或 <code>catch</code> 块中遇到 <code>return</code> 语句时，<code>finally</code> 语句块将在方法返回之前被执行。</li>
</ul>
<p><strong>注意</strong>：<strong>不要在 finally 语句块中使用 return!</strong> finall语句中的return值会覆盖try语句中的返回值。</p>
<hr>
<h3 id="反射"><a href="#反射" class="headerlink" title="反射"></a>反射</h3><h4 id="获取Class对象的四种方法"><a href="#获取Class对象的四种方法" class="headerlink" title="获取Class对象的四种方法"></a>获取Class对象的四种方法</h4><p><strong>1. 知道具体类的情况下可以使用：</strong></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Class alunbarClass = TargetObject.class;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><strong>2. 通过 <code>Class.forName()</code>传入类的全路径获取：</strong></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Class alunbarClass1 = Class.forName(&quot;cn.javaguide.TargetObject&quot;);</span><br></pre></td></tr></table></figure>

<p><strong>3. 通过对象实例<code>instance.getClass()</code>获取：</strong></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">TargetObject o = new TargetObject();</span><br><span class="line">Class alunbarClass2 = o.getClass();</span><br></pre></td></tr></table></figure>

<p><strong>4. 通过类加载器<code>xxxClassLoader.loadClass()</code>传入类路径获取:</strong></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ClassLoader.getSystemClassLoader().loadClass(&quot;cn.javaguide.TargetObject&quot;);</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>通过类加载器获取 Class 对象不会进行初始化，意味着不进行包括初始化等一系列步骤，静态代码块和静态对象不会得到执行</p>
<hr>
<h3 id="注解"><a href="#注解" class="headerlink" title="注解"></a>注解</h3><h4 id="注解的解析方法有哪几种-注解的解析方法有哪几种？"><a href="#注解的解析方法有哪几种-注解的解析方法有哪几种？" class="headerlink" title="注解的解析方法有哪几种)注解的解析方法有哪几种？"></a>注解的解析方法有哪几种)注解的解析方法有哪几种？</h4><p><strong>编译期直接扫描</strong> ：编译器在编译 Java 代码的时候扫描对应的注解并处理，比如某个方法使用<code>@Override</code> 注解，编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。</p>
<p><strong>运行期通过反射处理</strong> ：像框架中自带的注解(比如 Spring 框架的 <code>@Value</code> 、<code>@Component</code>)都是通过反射来进行处理的。</p>
<hr>
<h3 id="SIP"><a href="#SIP" class="headerlink" title="SIP"></a>SIP</h3><h4 id="SPI-和-API-有什么区别？"><a href="#SPI-和-API-有什么区别？" class="headerlink" title="SPI 和 API 有什么区别？"></a>SPI 和 API 有什么区别？</h4><p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/basis/spi/1ebd1df862c34880bc26b9d494535b3dtplv-k3u1fbpfcp-watermark.png" alt="img"></p>
<hr>
<h3 id="序列化和返序列化"><a href="#序列化和返序列化" class="headerlink" title="序列化和返序列化"></a>序列化和返序列化</h3><ul>
<li><strong>序列化</strong>： 将数据结构或对象转换成二进制字节流的过程</li>
<li><strong>反序列化</strong>：将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程</li>
</ul>
<h5 id="序列化协议对应于-TCP-x2F-IP-4-层模型的哪一层？"><a href="#序列化协议对应于-TCP-x2F-IP-4-层模型的哪一层？" class="headerlink" title="序列化协议对应于 TCP&#x2F;IP  4 层模型的哪一层？"></a>序列化协议对应于 TCP&#x2F;IP  4 层模型的哪一层？</h5><p><img src="G:\GitDocument\Study\images\面试.assets\image-20230123153850477.png" alt="image-20230123153850477"></p>
<h5 id="为什么不推荐使用-JDK-自带的序列化？"><a href="#为什么不推荐使用-JDK-自带的序列化？" class="headerlink" title="为什么不推荐使用 JDK 自带的序列化？"></a>为什么不推荐使用 JDK 自带的序列化？</h5><p><strong>不支持跨语言调用</strong> : 如果调用的是其他语言开发的服务的时候就不支持了。</p>
<p><strong>性能差</strong> ：相比于其他序列化框架性能更低，主要原因是序列化之后的字节数组体积较大，导致传输成本加大。</p>
<p><strong>存在安全问题</strong> ：序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制，那么攻击者即可通过构造恶意输入，让反序列化产生非预期的对象，在此过程中执行构造的任意代码。</p>
<hr>
<h3 id="I-x2F-O"><a href="#I-x2F-O" class="headerlink" title="I&#x2F;O"></a>I&#x2F;O</h3><p>IO 流在 Java 中分为输入流和输出流，而根据数据的处理方式又分为<strong>字节流</strong>和<strong>字符流</strong>。</p>
<ul>
<li><code>InputStream</code>&#x2F;<code>Reader</code>: 所有的输入流的基类，前者是字节输入流，后者是字符输入流。</li>
<li><code>OutputStream</code>&#x2F;<code>Writer</code>: 所有输出流的基类，前者是字节输出流，后者是字符输出流。</li>
</ul>
<h4 id="I-x2F-O-流为什么要分为字节流和字符流呢"><a href="#I-x2F-O-流为什么要分为字节流和字符流呢" class="headerlink" title="I&#x2F;O 流为什么要分为字节流和字符流呢?"></a>I&#x2F;O 流为什么要分为字节流和字符流呢?</h4><p><strong>不管是文件读写还是网络发送接收，信息的最小存储单元都是字节，那为什么 I&#x2F;O 流操作要分为字节流操作和字符流操作呢？</strong></p>
<ul>
<li>字符流是由 Java 虚拟机将字节转换得到的，这个过程还算是比较耗时；</li>
<li>如果我们不知道编码类型的话，使用字节流的过程中很容易出现乱码问题。</li>
</ul>
<hr>
<h4 id="Java-中-3-种常见-IO-模型"><a href="#Java-中-3-种常见-IO-模型" class="headerlink" title="Java 中 3 种常见 IO 模型"></a>Java 中 3 种常见 IO 模型</h4><h5 id="BIO-Blocking-I-x2F-O"><a href="#BIO-Blocking-I-x2F-O" class="headerlink" title="BIO (Blocking I&#x2F;O)"></a>BIO (Blocking I&#x2F;O)</h5><p><strong>BIO 属于同步阻塞 IO 模型</strong> 。</p>
<p>同步阻塞 IO 模型中，应用程序发起 read 调用后，会一直阻塞，直到内核把数据拷贝到用户空间。</p>
<p><img src="G:\GitDocument\Study\images\面试.assets\image-20230123164932422.png" alt="image-20230123164932422"></p>
<h5 id="NIO-Non-blocking-x2F-New-I-x2F-O"><a href="#NIO-Non-blocking-x2F-New-I-x2F-O" class="headerlink" title="NIO (Non-blocking&#x2F;New I&#x2F;O)"></a>NIO (Non-blocking&#x2F;New I&#x2F;O)</h5><p><strong>同步非阻塞 IO 模型</strong></p>
<p><img src="G:\GitDocument\Study\images\面试.assets\image-20230123165220480.png" alt="image-20230123165220480"></p>
<p><strong>应用程序不断进行 I&#x2F;O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。</strong></p>
<p><strong>I&#x2F;O 多路复用模型</strong> </p>
<p><img src="G:\GitDocument\Study\images\面试.assets\image-20230123165507701.png" alt="image-20230123165507701"></p>
<p>IO 多路复用模型中，线程首先发起 select 调用，询问内核数据是否准备就绪，等内核把数据准备好了，用户线程再发起 read 调用。read 调用的过程（数据从内核空间 -&gt; 用户空间）还是阻塞的。</p>
<p>目前支持 IO 多路复用的系统调用，有 select，epoll 等等。select 系统调用，目前几乎在所有的操作系统上都有支持。</p>
<ul>
<li><strong>select 调用</strong> ：内核提供的系统调用，它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持。</li>
<li><strong>epoll 调用</strong> ：linux 2.6 内核，属于 select 调用的增强版本，优化了 IO 的执行效率。</li>
</ul>
<p><strong>IO 多路复用模型，通过减少无效的系统调用，减少了对 CPU 资源的消耗。</strong></p>
<p><strong>选择器 ( Selector )</strong></p>
<p>可以被称为 <strong>多路复用器</strong>。通过它，只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后，才会为其服务。</p>
<h5 id="AIO-Asynchronous-I-x2F-O"><a href="#AIO-Asynchronous-I-x2F-O" class="headerlink" title="AIO (Asynchronous I&#x2F;O)"></a>AIO (Asynchronous I&#x2F;O)</h5><p>异步 IO 是基于事件和回调机制实现的，也就是应用操作之后会直接返回，不会堵塞在那里，当后台处理完成，操作系统会通知相应的线程进行后续的操作。</p>
<p><img src="G:\GitDocument\Study\images\面试.assets\image-20230123170648107.png" alt="image-20230123170648107"></p>
<h5 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h5><p><img src="G:\GitDocument\Study\images\面试.assets\image-20230123170752923.png" alt="image-20230123170752923"></p>
<hr>
<h3 id="语法糖"><a href="#语法糖" class="headerlink" title="语法糖"></a>语法糖</h3><h4 id="泛型"><a href="#泛型" class="headerlink" title="泛型"></a>泛型</h4><p>对于 Java 虚拟机来说，他根本不认识<code>Map&lt;String, String&gt; map</code>这样的语法。需要在编译阶段通过类型擦除的方式进行解语法糖。</p>
<p><strong>当泛型内包含静态变量</strong></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">public class StaticTest&#123;</span><br><span class="line">    public static void main(String[] args)&#123;</span><br><span class="line">        GT&lt;Integer&gt; gti = new GT&lt;Integer&gt;();</span><br><span class="line">        gti.var=1;</span><br><span class="line">        GT&lt;String&gt; gts = new GT&lt;String&gt;();</span><br><span class="line">        gts.var=2;</span><br><span class="line">        System.out.println(gti.var);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line">class GT&lt;T&gt;&#123;</span><br><span class="line">    public static int var=0;</span><br><span class="line">    public void nothing(T x)&#123;&#125;</span><br><span class="line">&#125;</span><br><span class="line">输出 :2</span><br></pre></td></tr></table></figure>



<hr>
<h4 id="自动装箱与拆箱-1"><a href="#自动装箱与拆箱-1" class="headerlink" title="自动装箱与拆箱"></a>自动装箱与拆箱</h4><p>装箱过程是通过调用包装器的 <code>valueOf</code> 方法实现的，而拆箱过程是通过调用包装器的<code>xxxValue</code>方法实现的。</p>
<hr>
<h4 id="可变长参数"><a href="#可变长参数" class="headerlink" title="可变长参数"></a>可变长参数</h4><p>可变参数(<code>variable arguments</code>)是在 Java 1.5 中引入的一个特性。它允许一个方法把任意数量的值作为参数。</p>
<p>从反编译后代码可以看出，可变参数在被使用的时候，他首先会创建一个数组，数组的长度就是调用该方法是传递的实参的个数，然后再把参数值全部放到这个数组当中，然后再把这个数组作为参数传递到被调用的方法中。</p>
<hr>
<h4 id="x3D-x3D-枚举-x3D-x3D"><a href="#x3D-x3D-枚举-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;枚举&#x3D;&#x3D;"></a>&#x3D;&#x3D;枚举&#x3D;&#x3D;</h4><p><strong>当我们使用<code>enum</code>来定义一个枚举类型的时候，编译器会自动帮我们创建一个<code>final</code>类型的类继承<code>Enum</code>类，所以枚举类型不能被继承。</strong></p>
<hr>
<h4 id="条件编译"><a href="#条件编译" class="headerlink" title="条件编译"></a>条件编译</h4><p>Java 语法的条件编译，是通过判断条件为常量的 if 语句实现的。其原理也是 Java 语言的语法糖。根据 if 判断条件的真假，编译器直接把分支为 false 的代码块消除。通过该方式实现的条件编译，必须在方法体内实现，而无法在正整个 Java 类的结构或者类的属性上进行条件编译，这与 C&#x2F;C++的条件编译相比，确实更有局限性。在 Java 语言设计之初并没有引入条件编译的功能，虽有局限，但是总比没有更强。</p>
<hr>
<h4 id="断言"><a href="#断言" class="headerlink" title="断言"></a>断言</h4><p>断言的底层实现就是 if 语言，如果断言结果为 true，则什么都不做，程序继续执行，如果断言结果为 false，则程序抛出 AssertError 来打断程序的执行。</p>
<hr>
<h4 id="for-each"><a href="#for-each" class="headerlink" title="for-each"></a>for-each</h4><p>for-each 的实现原理其实就是使用了普通的 for 循环和迭代器。</p>
<hr>
<h4 id="try-with-resource"><a href="#try-with-resource" class="headerlink" title="try-with-resource"></a>try-with-resource</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">public static void main(String[] args) &#123;</span><br><span class="line">    BufferedReader br = null;</span><br><span class="line">    try &#123;</span><br><span class="line">        String line;</span><br><span class="line">        br = new BufferedReader(new FileReader(&quot;d:\\hollischuang.xml&quot;));</span><br><span class="line">        while ((line = br.readLine()) != null) &#123;</span><br><span class="line">            System.out.println(line);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; catch (IOException e) &#123;</span><br><span class="line">        // handle exception</span><br><span class="line">    &#125; finally &#123;</span><br><span class="line">        try &#123;</span><br><span class="line">            if (br != null) &#123;</span><br><span class="line">                br.close();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; catch (IOException ex) &#123;</span><br><span class="line">            // handle exception</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><strong>改写</strong></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">public static void main(String... args) &#123;</span><br><span class="line">    try (BufferedReader br = new BufferedReader(new FileReader(&quot;d:\\ hollischuang.xml&quot;))) &#123;</span><br><span class="line">        String line;</span><br><span class="line">        while ((line = br.readLine()) != null) &#123;</span><br><span class="line">            System.out.println(line);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; catch (IOException e) &#123;</span><br><span class="line">        // handle exception</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="Lambda-表达式"><a href="#Lambda-表达式" class="headerlink" title="Lambda 表达式"></a>Lambda 表达式</h4><p>Labmda 表达式不是匿名内部类的语法糖，但是他也是一个语法糖。实现方式其实是依赖了几个 JVM 底层提供的 lambda 相关 api。</p>
<p><strong>所以，lambda 表达式的实现其实是依赖了一些底层的 api，在编译阶段，编译器会把 lambda 表达式进行解糖，转换成调用内部 api 的方式。</strong></p>
<hr>
<h2 id="重要知识点"><a href="#重要知识点" class="headerlink" title="重要知识点"></a>重要知识点</h2><h3 id="JAVA中只传递值"><a href="#JAVA中只传递值" class="headerlink" title="JAVA中只传递值"></a>JAVA中只传递值</h3><p>Java 中将实参传递给方法（或函数）的方式是 <strong>值传递</strong> ：</p>
<ul>
<li>如果参数是基本类型的话，很简单，传递的就是基本类型的字面量值的拷贝，会&#x3D;&#x3D;创建副本&#x3D;&#x3D;。</li>
<li>如果参数是引用类型，传递的就是实参所引用的对象在堆中地址值的拷贝，同样也会创建副本。<ul>
<li>只要不对地址值指向的数据做修改，原始参数的值不会改变</li>
</ul>
</li>
</ul>
<hr>
<h3 id="Java代理模式详解"><a href="#Java代理模式详解" class="headerlink" title="Java代理模式详解"></a>Java代理模式详解</h3><p>我们使用代理对象来代替对真实对象(real object)的访问，这样就可以在不修改原目标对象的前提下，提供额外的功能操作，扩展目标对象的功能。</p>
<p>代理模式的主要作用是扩展目标对象的功能，比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。</p>
<h4 id="静态代理"><a href="#静态代理" class="headerlink" title="静态代理"></a>静态代理</h4><p>静态代理中，我们对目标对象的每个方法的增强都是手动完成的（*后面会具体演示代码*），非常不灵活（*比如接口一旦新增加方法，目标对象和代理对象都要进行修改*）且麻烦(*需要对每个目标类都单独写一个代理类*）。</p>
<p>从 JVM 层面来说， 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。</p>
<h5 id="实现步骤"><a href="#实现步骤" class="headerlink" title="实现步骤"></a>实现步骤</h5><ul>
<li>定义一个接口及其实现类；</li>
<li>创建一个代理类同样实现这个接口</li>
<li>将目标对象注入进代理类，然后在代理类的对应方法调用目标类中的对应方法。这样的话，我们就可以通过代理类屏蔽对目标对象的访问，并且可以在目标方法执行前后做一些自己想做的事情。</li>
</ul>
<h4 id="动态代理"><a href="#动态代理" class="headerlink" title="动态代理"></a>动态代理</h4><p>不需要针对每个目标类都单独创建一个代理类，并且也不需要必须实现接口，可以直接代理实现类( <em>CGLIB 动态代理机制</em>)。</p>
<p><strong>从 JVM 角度来说，动态代理是在运行时动态生成类字节码，并加载到 JVM 中的。</strong></p>
<p>动态代理在我们日常开发中使用的相对较少，但是在框架中的几乎是必用的一门技术。学会了动态代理之后，对于我们理解和学习各种框架的原理也非常有帮助。</p>
<h5 id="JDK-动态代理机制"><a href="#JDK-动态代理机制" class="headerlink" title="JDK 动态代理机制"></a>JDK 动态代理机制</h5><h6 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h6><p><strong>在 Java 动态代理机制中 <code>InvocationHandler</code> 接口和 <code>Proxy</code> 类是核心。</strong></p>
<h6 id="使用步骤"><a href="#使用步骤" class="headerlink" title="使用步骤"></a>使用步骤</h6><ul>
<li>定义一个接口及其实现类；</li>
<li>自定义 <code>InvocationHandler</code> 并重写<code>invoke</code>方法，在 <code>invoke</code> 方法中我们会调用原生方法（被代理类的方法）并自定义一些处理逻辑；</li>
<li>通过 <code>Proxy.newProxyInstance(ClassLoader loader,Class&lt;?&gt;[] interfaces,InvocationHandler h)</code> 方法创建代理对象</li>
</ul>
<h5 id="CGLIB-动态代理机制"><a href="#CGLIB-动态代理机制" class="headerlink" title="CGLIB 动态代理机制"></a>CGLIB 动态代理机制</h5><h6 id="介绍-1"><a href="#介绍-1" class="headerlink" title="介绍"></a>介绍</h6><p>JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。为了解决这个问题，我们可以用 CGLIB 动态代理机制来避免。</p>
<p><strong>在 CGLIB 动态代理机制中 <code>MethodInterceptor</code> 接口和 <code>Enhancer</code> 类是核心。</strong></p>
<h6 id="实现步骤-1"><a href="#实现步骤-1" class="headerlink" title="实现步骤"></a>实现步骤</h6><ul>
<li>定义一个类；</li>
<li>自定义 <code>MethodInterceptor</code> 并重写 <code>intercept</code> 方法，<code>intercept</code> 用于拦截增强被代理类的方法，和 JDK 动态代理中的 <code>invoke</code> 方法类似；</li>
<li>通过 <code>Enhancer</code> 类的 <code>create()</code>创建代理类</li>
</ul>
<h5 id="JDK-动态代理和-CGLIB-动态代理对比"><a href="#JDK-动态代理和-CGLIB-动态代理对比" class="headerlink" title="JDK 动态代理和 CGLIB 动态代理对比"></a>JDK 动态代理和 CGLIB 动态代理对比</h5><ul>
<li><strong>JDK 动态代理只能代理实现了接口的类或者直接代理接口，而 CGLIB 可以代理未实现任何接口的类。</strong> 另外， CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用，因此不能代理声明为 final 类型的类和方法。</li>
<li>就二者的效率来说，大部分情况都是 JDK 动态代理更优秀，随着 JDK 版本的升级，这个优势更加明显。</li>
</ul>
<h4 id="静态代理和动态代理的对比"><a href="#静态代理和动态代理的对比" class="headerlink" title="静态代理和动态代理的对比"></a>静态代理和动态代理的对比</h4><ul>
<li><strong>灵活性</strong> ：动态代理更加灵活，不需要必须实现接口，可以直接代理实现类，并且可以不需要针对每个目标类都创建一个代理类。另外，静态代理中，接口一旦新增加方法，目标对象和代理对象都要进行修改，这是非常麻烦的！</li>
<li><strong>JVM 层面</strong> ：静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码，并加载到 JVM 中的。</li>
</ul>
<hr>
<h3 id="BigDecimal详解"><a href="#BigDecimal详解" class="headerlink" title="BigDecimal详解"></a>BigDecimal详解</h3><h4 id="等值比较问题"><a href="#等值比较问题" class="headerlink" title="等值比较问题"></a>等值比较问题</h4><p> <code>equals()</code> 方法不仅仅会比较值的大小（value）还会比较精度（scale），而 <code>compareTo()</code> 方法比较的时候会忽略精度。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">1.0 的 scale 是 1，1 的 scale 是 0，因此 a.equals(b) 的结果是 false。</span><br></pre></td></tr></table></figure>

<h4 id="工具类"><a href="#工具类" class="headerlink" title="工具类"></a>工具类</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br></pre></td><td class="code"><pre><span class="line">import java.math.BigDecimal;</span><br><span class="line">import java.math.RoundingMode;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * 简化BigDecimal计算的小工具类</span><br><span class="line"> */</span><br><span class="line">public class BigDecimalUtil &#123;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 默认除法运算精度</span><br><span class="line">     */</span><br><span class="line">    private static final int DEF_DIV_SCALE = 10;</span><br><span class="line"></span><br><span class="line">    private BigDecimalUtil() &#123;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 提供精确的加法运算。</span><br><span class="line">     *</span><br><span class="line">     * @param v1 被加数</span><br><span class="line">     * @param v2 加数</span><br><span class="line">     * @return 两个参数的和</span><br><span class="line">     */</span><br><span class="line">    public static double add(double v1, double v2) &#123;</span><br><span class="line">        BigDecimal b1 = BigDecimal.valueOf(v1);</span><br><span class="line">        BigDecimal b2 = BigDecimal.valueOf(v2);</span><br><span class="line">        return b1.add(b2).doubleValue();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 提供精确的减法运算。</span><br><span class="line">     *</span><br><span class="line">     * @param v1 被减数</span><br><span class="line">     * @param v2 减数</span><br><span class="line">     * @return 两个参数的差</span><br><span class="line">     */</span><br><span class="line">    public static double subtract(double v1, double v2) &#123;</span><br><span class="line">        BigDecimal b1 = BigDecimal.valueOf(v1);</span><br><span class="line">        BigDecimal b2 = BigDecimal.valueOf(v2);</span><br><span class="line">        return b1.subtract(b2).doubleValue();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 提供精确的乘法运算。</span><br><span class="line">     *</span><br><span class="line">     * @param v1 被乘数</span><br><span class="line">     * @param v2 乘数</span><br><span class="line">     * @return 两个参数的积</span><br><span class="line">     */</span><br><span class="line">    public static double multiply(double v1, double v2) &#123;</span><br><span class="line">        BigDecimal b1 = BigDecimal.valueOf(v1);</span><br><span class="line">        BigDecimal b2 = BigDecimal.valueOf(v2);</span><br><span class="line">        return b1.multiply(b2).doubleValue();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 提供（相对）精确的除法运算，当发生除不尽的情况时，精确到</span><br><span class="line">     * 小数点以后10位，以后的数字四舍五入。</span><br><span class="line">     *</span><br><span class="line">     * @param v1 被除数</span><br><span class="line">     * @param v2 除数</span><br><span class="line">     * @return 两个参数的商</span><br><span class="line">     */</span><br><span class="line">    public static double divide(double v1, double v2) &#123;</span><br><span class="line">        return divide(v1, v2, DEF_DIV_SCALE);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 提供（相对）精确的除法运算。当发生除不尽的情况时，由scale参数指</span><br><span class="line">     * 定精度，以后的数字四舍五入。</span><br><span class="line">     *</span><br><span class="line">     * @param v1    被除数</span><br><span class="line">     * @param v2    除数</span><br><span class="line">     * @param scale 表示表示需要精确到小数点以后几位。</span><br><span class="line">     * @return 两个参数的商</span><br><span class="line">     */</span><br><span class="line">    public static double divide(double v1, double v2, int scale) &#123;</span><br><span class="line">        if (scale &lt; 0) &#123;</span><br><span class="line">            throw new IllegalArgumentException(</span><br><span class="line">                    &quot;The scale must be a positive integer or zero&quot;);</span><br><span class="line">        &#125;</span><br><span class="line">        BigDecimal b1 = BigDecimal.valueOf(v1);</span><br><span class="line">        BigDecimal b2 = BigDecimal.valueOf(v2);</span><br><span class="line">        return b1.divide(b2, scale, RoundingMode.HALF_UP).doubleValue();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 提供精确的小数位四舍五入处理。</span><br><span class="line">     *</span><br><span class="line">     * @param v     需要四舍五入的数字</span><br><span class="line">     * @param scale 小数点后保留几位</span><br><span class="line">     * @return 四舍五入后的结果</span><br><span class="line">     */</span><br><span class="line">    public static double round(double v, int scale) &#123;</span><br><span class="line">        if (scale &lt; 0) &#123;</span><br><span class="line">            throw new IllegalArgumentException(</span><br><span class="line">                    &quot;The scale must be a positive integer or zero&quot;);</span><br><span class="line">        &#125;</span><br><span class="line">        BigDecimal b = BigDecimal.valueOf(v);</span><br><span class="line">        BigDecimal one = new BigDecimal(&quot;1&quot;);</span><br><span class="line">        return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 提供精确的类型转换(Float)</span><br><span class="line">     *</span><br><span class="line">     * @param v 需要被转换的数字</span><br><span class="line">     * @return 返回转换结果</span><br><span class="line">     */</span><br><span class="line">    public static float convertToFloat(double v) &#123;</span><br><span class="line">        BigDecimal b = new BigDecimal(v);</span><br><span class="line">        return b.floatValue();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 提供精确的类型转换(Int)不进行四舍五入</span><br><span class="line">     *</span><br><span class="line">     * @param v 需要被转换的数字</span><br><span class="line">     * @return 返回转换结果</span><br><span class="line">     */</span><br><span class="line">    public static int convertsToInt(double v) &#123;</span><br><span class="line">        BigDecimal b = new BigDecimal(v);</span><br><span class="line">        return b.intValue();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 提供精确的类型转换(Long)</span><br><span class="line">     *</span><br><span class="line">     * @param v 需要被转换的数字</span><br><span class="line">     * @return 返回转换结果</span><br><span class="line">     */</span><br><span class="line">    public static long convertsToLong(double v) &#123;</span><br><span class="line">        BigDecimal b = new BigDecimal(v);</span><br><span class="line">        return b.longValue();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 返回两个数中大的一个值</span><br><span class="line">     *</span><br><span class="line">     * @param v1 需要被对比的第一个数</span><br><span class="line">     * @param v2 需要被对比的第二个数</span><br><span class="line">     * @return 返回两个数中大的一个值</span><br><span class="line">     */</span><br><span class="line">    public static double returnMax(double v1, double v2) &#123;</span><br><span class="line">        BigDecimal b1 = new BigDecimal(v1);</span><br><span class="line">        BigDecimal b2 = new BigDecimal(v2);</span><br><span class="line">        return b1.max(b2).doubleValue();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 返回两个数中小的一个值</span><br><span class="line">     *</span><br><span class="line">     * @param v1 需要被对比的第一个数</span><br><span class="line">     * @param v2 需要被对比的第二个数</span><br><span class="line">     * @return 返回两个数中小的一个值</span><br><span class="line">     */</span><br><span class="line">    public static double returnMin(double v1, double v2) &#123;</span><br><span class="line">        BigDecimal b1 = new BigDecimal(v1);</span><br><span class="line">        BigDecimal b2 = new BigDecimal(v2);</span><br><span class="line">        return b1.min(b2).doubleValue();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /**</span><br><span class="line">     * 精确对比两个数字</span><br><span class="line">     *</span><br><span class="line">     * @param v1 需要被对比的第一个数</span><br><span class="line">     * @param v2 需要被对比的第二个数</span><br><span class="line">     * @return 如果两个数一样则返回0，如果第一个数比第二个数大则返回1，反之返回-1</span><br><span class="line">     */</span><br><span class="line">    public static int compareTo(double v1, double v2) &#123;</span><br><span class="line">        BigDecimal b1 = BigDecimal.valueOf(v1);</span><br><span class="line">        BigDecimal b2 = BigDecimal.valueOf(v2);</span><br><span class="line">        return b1.compareTo(b2);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<hr>
<h3 id="Java魔法类Unsafe详解"><a href="#Java魔法类Unsafe详解" class="headerlink" title="Java魔法类Unsafe详解"></a>Java魔法类Unsafe详解</h3><h4 id="Unsafe功能"><a href="#Unsafe功能" class="headerlink" title="Unsafe功能"></a>Unsafe功能</h4><h5 id="内存操作"><a href="#内存操作" class="headerlink" title="内存操作"></a>内存操作</h5><p>通过这种方式分配的内存属于 堆外内存 ，是无法进行垃圾回收的，需要我们把这些内存当做一种资源去手动调用<code>freeMemory</code>方法进行释放，否则会产生内存泄漏。通用的操作内存方式是在<code>try</code>中执行对内存的操作，最终在<code>finally</code>块中进行内存的释放。</p>
<h6 id="为什么要使用堆外内存？"><a href="#为什么要使用堆外内存？" class="headerlink" title="为什么要使用堆外内存？"></a><strong>为什么要使用堆外内存？</strong></h6><ul>
<li>对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM，所以当我们使用堆外内存时，即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。</li>
<li>提升程序 I&#x2F;O 操作的性能。通常在 I&#x2F;O 通信过程中，会存在堆内内存到堆外内存的数据拷贝操作，对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据，都建议存储到堆外内存</li>
</ul>
<h5 id="内存屏障"><a href="#内存屏障" class="headerlink" title="内存屏障"></a>内存屏障</h5><p><code>Unsafe</code> 还提供了 <strong>volatile 读写</strong>和<strong>有序写入</strong>方法。</p>
<p><code>volatile</code>读写具有更高的成本，因为它需要保证可见性和有序性。在执行<code>get</code>操作时，会强制从主存中获取属性值，在使用<code>put</code>方法设置属性值时，会强制将值更新到主存中，从而保证这些变更对其他线程是可见的。</p>
<p>有序写入的成本相对<code>volatile</code>较低，因为它只保证写入时的有序性，而不保证可见性，也就是一个线程写入的值不能保证其他线程立即可见。为了解决这里的差异性，需要对内存屏障的知识点再进一步进行补充，首先需要了解两个指令的概念：</p>
<ul>
<li><code>Load</code>：将主内存中的数据拷贝到处理器的缓存中</li>
<li><code>Store</code>：将处理器缓存的数据刷新到主内存中</li>
</ul>
<h5 id="对象操作"><a href="#对象操作" class="headerlink" title="对象操作"></a>对象操作</h5><h5 id="数据操作"><a href="#数据操作" class="headerlink" title="数据操作"></a>数据操作</h5><h5 id="CAS-操作"><a href="#CAS-操作" class="headerlink" title="CAS 操作"></a>CAS 操作</h5><p>CAS 即比较并替换（Compare And Swap)，是实现并发算法时常用到的一种技术。CAS 操作包含三个操作数——内存位置、预期原值及新值。</p>
<h5 id="线程调度"><a href="#线程调度" class="headerlink" title="线程调度"></a>线程调度</h5><p>方法 <code>park</code>、<code>unpark</code> 即可实现线程的挂起与恢复，将一个线程进行挂起是通过 <code>park</code> 方法实现的，调用 <code>park</code> 方法后，线程将一直阻塞直到超时或者中断等条件出现；<code>unpark</code> 可以终止一个挂起的线程，使其恢复正常。</p>
<p><strong>Java 锁和同步器框架的核心类</strong> <code>AbstractQueuedSynchronizer</code> (AQS)，就是通过调用<code>LockSupport.park()</code>和<code>LockSupport.unpark()</code>实现线程的阻塞和唤醒的，而 <code>LockSupport</code> 的 <code>park</code> 、<code>unpark</code> 方法实际是调用 <code>Unsafe</code> 的 <code>park</code> 、<code>unpark</code> 方式实现的。</p>
<p><img src="G:\GitDocument\Study\images\面试.assets\image-20230124200833608.png" alt="image-20230124200833608"></p>
<h5 id="Class-操作"><a href="#Class-操作" class="headerlink" title="Class 操作"></a>Class 操作</h5><h5 id="系统信息"><a href="#系统信息" class="headerlink" title="系统信息"></a>系统信息</h5><h3 id="Java-SPI-机制详解"><a href="#Java-SPI-机制详解" class="headerlink" title="Java SPI 机制详解"></a>Java SPI 机制详解</h3><p>SPI 机制的具体实现本质上还是通过反射完成的。即：<strong>我们按照规定将要暴露对外使用的具体实现类在 <code>META-INF/services/</code> 文件下声明。</strong></p>
<h2 id="集合"><a href="#集合" class="headerlink" title="集合"></a>集合</h2><h3 id="Java集合上"><a href="#Java集合上" class="headerlink" title="Java集合上"></a>Java集合上</h3><p>Java 集合， 也叫作容器，主要是由两大接口派生而来：一个是 <code>Collection</code>接口，主要用于存放单一元素；另一个是 <code>Map</code> 接口，主要用于存放键值对。对于<code>Collection</code> 接口，下面又有三个主要的子接口：<code>List</code>、<code>Set</code> 和 <code>Queue</code>。</p>
<p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/collection/java-collection-hierarchy.png" alt="img"></p>
<h4 id="集合框架底层数据结构总结"><a href="#集合框架底层数据结构总结" class="headerlink" title="集合框架底层数据结构总结"></a>集合框架底层数据结构总结</h4><h5 id="List"><a href="#List" class="headerlink" title="List"></a>List</h5><ul>
<li><code>ArrayList</code>： <code>Object[]</code> 数组</li>
<li><code>Vector</code>：<code>Object[]</code> 数组</li>
<li><code>LinkedList</code>： 双向链表(JDK1.6 之前为循环链表，JDK1.7 取消了循环)</li>
</ul>
<h5 id="Set"><a href="#Set" class="headerlink" title="Set"></a>Set</h5><ul>
<li><code>HashSet</code>(无序，唯一): 基于 <code>HashMap</code> 实现的，底层采用 <code>HashMap</code> 来保存元素</li>
<li><code>LinkedHashSet</code>: <code>LinkedHashSet</code> 是 <code>HashSet</code> 的子类，并且其内部是通过 <code>LinkedHashMap</code> 来实现的。有点类似于我们之前说的 <code>LinkedHashMap</code> 其内部是基于 <code>HashMap</code> 实现一样，不过还是有一点点区别的</li>
<li><code>TreeSet</code>(有序，唯一): 红黑树(自平衡的排序二叉树)</li>
</ul>
<h5 id="Queue"><a href="#Queue" class="headerlink" title="Queue"></a>Queue</h5><ul>
<li><code>PriorityQueue</code>: <code>Object[]</code> 数组来实现二叉堆</li>
<li><code>ArrayQueue</code>: <code>Object[]</code> 数组 + 双指针</li>
</ul>
<h5 id="Map"><a href="#Map" class="headerlink" title="Map"></a>Map</h5><ul>
<li><code>HashMap</code>： JDK1.8 之前 <code>HashMap</code> 由数组+链表组成的，数组是 <code>HashMap</code> 的主体，链表则是主要为了解决哈希冲突而存在的（“拉链法”解决冲突）。JDK1.8 以后在解决哈希冲突时有了较大的变化，当链表长度大于阈值（默认为 8）（将链表转换成红黑树前会判断，如果当前数组的长度小于 64，那么会选择先进行数组扩容，而不是转换为红黑树）时，将链表转化为红黑树，以减少搜索时间</li>
<li><code>LinkedHashMap</code>： <code>LinkedHashMap</code> 继承自 <code>HashMap</code>，所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外，<code>LinkedHashMap</code> 在上面结构的基础上，增加了一条双向链表，使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作，实现了访问顺序相关逻辑。详细可以查看：<a target="_blank" rel="noopener" href="https://www.imooc.com/article/22931">《LinkedHashMap 源码详细分析（JDK1.8）》open in new window</a></li>
<li><code>Hashtable</code>： 数组+链表组成的，数组是 <code>Hashtable</code> 的主体，链表则是主要为了解决哈希冲突而存在的</li>
<li><code>TreeMap</code>： 红黑树（自平衡的排序二叉树）</li>
</ul>
<h4 id="Collection-子接口之-List"><a href="#Collection-子接口之-List" class="headerlink" title="Collection 子接口之 List"></a>Collection 子接口之 List</h4><h5 id="ArrayList-和-Vector-的区别"><a href="#ArrayList-和-Vector-的区别" class="headerlink" title="ArrayList 和 Vector 的区别?"></a>ArrayList 和 Vector 的区别?</h5><ul>
<li><code>ArrayList</code> 是 <code>List</code> 的主要实现类，底层使用 <code>Object[]</code>存储，适用于频繁的查找工作，线程不安全 ；</li>
<li><code>Vector</code> 是 <code>List</code> 的古老实现类，底层使用<code>Object[]</code> 存储，线程安全的。</li>
</ul>
<h5 id="ArrayList-与-LinkedList-区别"><a href="#ArrayList-与-LinkedList-区别" class="headerlink" title="ArrayList 与 LinkedList 区别?"></a>ArrayList 与 LinkedList 区别?</h5><ul>
<li><strong>是否保证线程安全：</strong> <code>ArrayList</code> 和 <code>LinkedList</code> 都是不同步的，也就是不保证线程安全；</li>
<li><strong>底层数据结构：</strong> <code>ArrayList</code> 底层使用的是 <strong><code>Object</code> 数组</strong>；<code>LinkedList</code> 底层使用的是 <strong>双向链表</strong> 数据结构（JDK1.6 之前为循环链表，JDK1.7 取消了循环。注意双向链表和双向循环链表的区别，下面有介绍到！）</li>
<li><strong>插入和删除是否受元素位置的影响：</strong><ul>
<li><code>ArrayList</code> 采用数组存储，所以插入和删除元素的时间复杂度受元素位置的影响。 比如：执行<code>add(E e)</code>方法的时候， <code>ArrayList</code> 会默认在将指定的元素追加到此列表的末尾，这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话（<code>add(int index, E element)</code>）时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位&#x2F;向前移一位的操作。</li>
<li><code>LinkedList</code> 采用链表存储，所以，如果是在头尾插入或者删除元素不受元素位置的影响（<code>add(E e)</code>、<code>addFirst(E e)</code>、<code>addLast(E e)</code>、<code>removeFirst()</code> 、 <code>removeLast()</code>），时间复杂度为 O(1)，如果是要在指定位置 <code>i</code> 插入和删除元素的话（<code>add(int index, E element)</code>，<code>remove(Object o)</code>）， 时间复杂度为 O(n) ，因为需要先移动到指定位置再插入。（若给定前驱节点，则插入删除的时间复杂度为O（1））</li>
</ul>
</li>
<li><strong>是否支持快速随机访问：</strong> <code>LinkedList</code> 不支持高效的随机元素访问，而 <code>ArrayList</code> 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于<code>get(int index)</code>方法)。</li>
<li><strong>内存空间占用：</strong> <code>ArrayList</code> 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间，而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间（因为要存放直接后继和直接前驱以及数据）。</li>
</ul>
<hr>
<h4 id="Collection-子接口之-Set"><a href="#Collection-子接口之-Set" class="headerlink" title="Collection 子接口之 Set"></a>Collection 子接口之 Set</h4><h5 id="comparable-和-Comparator-的区别"><a href="#comparable-和-Comparator-的区别" class="headerlink" title="comparable 和 Comparator 的区别"></a>comparable 和 Comparator 的区别</h5><ul>
<li><code>comparable</code> 接口实际上是出自<code>java.lang</code>包 它有一个 <code>compareTo(Object obj)</code>方法用来排序</li>
<li><code>comparator</code>接口实际上是出自 java.util 包它有一个<code>compare(Object obj1, Object obj2)</code>方法用来排序</li>
</ul>
<h5 id="比较-HashSet、LinkedHashSet-和-TreeSet-三者的异同"><a href="#比较-HashSet、LinkedHashSet-和-TreeSet-三者的异同" class="headerlink" title="比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同"></a>比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同</h5><ul>
<li><code>HashSet</code>、<code>LinkedHashSet</code> 和 <code>TreeSet</code> 都是 <code>Set</code> 接口的实现类，都能保证元素唯一，并且都不是线程安全的。</li>
<li>底层结构：<code>HashSet</code>、<code>LinkedHashSet</code> 和 <code>TreeSet</code> 的主要区别在于底层数据结构不同。<code>HashSet</code> 的底层数据结构是哈希表（基于 <code>HashMap</code> 实现）。<code>LinkedHashSet</code> 的底层数据结构是链表和哈希表，元素的插入和取出顺序满足 FIFO。<code>TreeSet</code> 底层数据结构是红黑树，元素是有序的，排序的方式有自然排序和定制排序。</li>
<li>应用场景：底层数据结构不同又导致这三者的应用场景不同。<code>HashSet</code> 用于不需要保证元素插入和取出顺序的场景，<code>LinkedHashSet</code> 用于保证元素的插入和取出顺序满足 FIFO 的场景，<code>TreeSet</code> 用于支持对元素自定义排序规则的场景。</li>
</ul>
<hr>
<h4 id="Collection-子接口之-Queue"><a href="#Collection-子接口之-Queue" class="headerlink" title="Collection 子接口之 Queue"></a>Collection 子接口之 Queue</h4><h5 id="Queue-与-Deque-的区别"><a href="#Queue-与-Deque-的区别" class="headerlink" title="Queue 与 Deque 的区别"></a>Queue 与 Deque 的区别</h5><p><strong>Queue</strong></p>
<p><code>Queue</code> 是单端队列，只能从一端插入元素，另一端删除元素，实现上一般遵循 <strong>先进先出（FIFO）</strong> 规则。</p>
<p><strong>因为容量问题而导致操作失败后处理方式的不同</strong> 可以分为两类方法: 一种在操作失败后会抛出异常，另一种则会返回特殊值。</p>
<p><img src="G:\GitDocument\Study\images\面试.assets\image-20230125164213075.png" alt="image-20230125164213075"></p>
<p><strong>Deque</strong></p>
<p><code>Deque</code> 是双端队列，在队列的两端均可以插入或删除元素。</p>
<p><code>Deque</code> 扩展了 <code>Queue</code> 的接口, 增加了在队首和队尾进行插入和删除的方法，同样根据失败后处理方式的不同分为两类：</p>
<p><img src="G:\GitDocument\Study\images\面试.assets\image-20230125164300381.png" alt="image-20230125164300381"></p>
<hr>
<h3 id="Java集合下"><a href="#Java集合下" class="headerlink" title="Java集合下"></a>Java集合下</h3><h4 id="Map-接口"><a href="#Map-接口" class="headerlink" title="Map 接口"></a>Map 接口</h4><h5 id="HashMap-和-Hashtable-的区别"><a href="#HashMap-和-Hashtable-的区别" class="headerlink" title="HashMap 和 Hashtable 的区别"></a>HashMap 和 Hashtable 的区别</h5><ul>
<li><strong>线程是否安全：</strong> <code>HashMap</code> 是非线程安全的，<code>Hashtable</code> 是线程安全的,因为 <code>Hashtable</code> 内部的方法基本都经过<code>synchronized</code> 修饰。（如果你要保证线程安全的话就使用 <code>ConcurrentHashMap</code> 吧！）；</li>
<li><strong>效率：</strong> 因为线程安全的问题，<code>HashMap</code> 要比 <code>Hashtable</code> 效率高一点。另外，<code>Hashtable</code> 基本被淘汰，不要在代码中使用它；</li>
<li><strong>对 Null key 和 Null value 的支持：</strong> <code>HashMap</code> 可以存储 null 的 key 和 value，但 null 作为键只能有一个，null 作为值可以有多个；Hashtable 不允许有 null 键和 null 值，否则会抛出 <code>NullPointerException</code>。</li>
<li><strong>初始容量大小和每次扩充容量大小的不同 ：</strong> ① 创建时如果不指定容量初始值，<code>Hashtable</code> 默认的初始大小为 11，之后每次扩充，容量变为原来的 2n+1。<code>HashMap</code> 默认的初始化大小为 16。之后每次扩充，容量变为原来的 2 倍。② 创建时如果给定了容量初始值，那么 <code>Hashtable</code> 会直接使用你给定的大小，而 <code>HashMap</code> 会将其扩充为 2 的幂次方大小（<code>HashMap</code> 中的<code>tableSizeFor()</code>方法保证，下面给出了源代码）。也就是说 <code>HashMap</code> 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。</li>
<li><strong>底层数据结构：</strong> JDK1.8 以后的 <code>HashMap</code> 在解决哈希冲突时有了较大的变化，&#x3D;&#x3D;当链表长度大于阈值（默认为 8）时，将链表转化为红黑树（将链表转换成红黑树前会判断，如果当前数组的长度小于 64，那么会选择先进行数组扩容，而不是转换为红黑树），以减少搜索时间&#x3D;&#x3D;（后文中我会结合源码对这一过程进行分析）。<code>Hashtable</code> 没有这样的机制。</li>
</ul>
<h5 id="HashMap-和-HashSet-区别"><a href="#HashMap-和-HashSet-区别" class="headerlink" title="HashMap 和 HashSet 区别"></a>HashMap 和 HashSet 区别</h5><p><code>HashSet</code> 底层就是基于 <code>HashMap</code> 实现的。（<code>HashSet</code> 的源码非常非常少，因为除了 <code>clone()</code>、<code>writeObject()</code>、<code>readObject()</code>是 <code>HashSet</code> 自己不得不实现之外，其他方法都是直接调用 <code>HashMap</code> 中的方法。</p>
<h5 id="HashMap-和-TreeMap-区别"><a href="#HashMap-和-TreeMap-区别" class="headerlink" title="HashMap 和 TreeMap 区别"></a>HashMap 和 TreeMap 区别</h5><p><code>TreeMap</code> 和<code>HashMap</code> 都继承自<code>AbstractMap</code> ，但是需要注意的是<code>TreeMap</code>它还实现了<code>NavigableMap</code>接口和<code>SortedMap</code> 接口。</p>
<p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/collection/treemap_hierarchy.png" alt="TreeMap 继承关系图"></p>
<p>实现 <code>NavigableMap</code> 接口让 <code>TreeMap</code> 有了对集合内元素的搜索的能力。</p>
<p>实现<code>SortedMap</code>接口让 <code>TreeMap</code> 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序，不过我们也可以指定排序的比较器。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">TreeMap&lt;Person, String&gt; treeMap = new TreeMap&lt;&gt;((person1, person2) -&gt; &#123;</span><br><span class="line">  int num = person1.getAge() - person2.getAge();</span><br><span class="line">  return Integer.compare(num, 0);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p><strong>综上，相比于<code>HashMap</code>来说 <code>TreeMap</code> 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。</strong></p>
<hr>
<h5 id="HashMap-的底层实现"><a href="#HashMap-的底层实现" class="headerlink" title="HashMap 的底层实现"></a>HashMap 的底层实现</h5><p>JDK1.8 之前 <code>HashMap</code> 底层是 <strong>数组和链表</strong> 结合在一起使用也就是 <strong>链表散列</strong></p>
<p>JDK1.8 之，后将链表转化为红黑树，以减少搜索时间。</p>
<hr>
<h5 id="HashMap-的长度为什么是-2-的幂次方"><a href="#HashMap-的长度为什么是-2-的幂次方" class="headerlink" title="HashMap 的长度为什么是 2 的幂次方"></a>HashMap 的长度为什么是 2 的幂次方</h5><p><strong>“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&amp;)操作（也就是说 hash%length&#x3D;&#x3D;hash&amp;(length-1)的前提是 length 是 2 的 n 次方；）。”</strong> 并且 **采用二进制位操作 &amp;，相对于%能够提高运算效率，这就解释了 HashMap 的长度为什么是 2 的幂次方。</p>
<hr>
<h5 id="JDK-1-7-和-JDK-1-8-的-ConcurrentHashMap-实现有什么不同？"><a href="#JDK-1-7-和-JDK-1-8-的-ConcurrentHashMap-实现有什么不同？" class="headerlink" title="JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同？"></a>JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同？</h5><ul>
<li><strong>线程安全实现方式</strong> ：JDK 1.7 采用 <code>Segment</code> 分段锁来保证安全， <code>Segment</code> 是继承自 <code>ReentrantLock</code>。JDK1.8 放弃了 <code>Segment</code> 分段锁的设计，采用 <code>Node + CAS + synchronized</code> 保证线程安全，锁粒度更细，<code>synchronized</code> 只锁定当前链表或红黑二叉树的首节点。</li>
<li><strong>Hash 碰撞解决方法</strong> : JDK 1.7 采用拉链法，JDK1.8 采用拉链法结合红黑树（链表长度超过一定阈值时，将链表转换为红黑树）。</li>
<li><strong>并发度</strong> ：JDK 1.7 最大并发度是 Segment 的个数，默认是 16。JDK 1.8 最大并发度是 Node 数组的大小，并发度更大。</li>
</ul>
<hr>
<h3 id="Java集合使用注意事项总结"><a href="#Java集合使用注意事项总结" class="headerlink" title="Java集合使用注意事项总结"></a>Java集合使用注意事项总结</h3><h4 id="集合遍历"><a href="#集合遍历" class="headerlink" title="集合遍历"></a>集合遍历</h4><blockquote>
<p>不要在 foreach 循环里进行元素的 <code>remove/add</code> 操作。remove 元素请使用 <code>Iterator</code> 方式，如果并发操作，需要对 <code>Iterator</code> 对象加锁。**</p>
</blockquote>
<hr>
<h4 id="集合转数组"><a href="#集合转数组" class="headerlink" title="集合转数组"></a>集合转数组</h4><blockquote>
<p>使用集合转数组的方法，必须使用集合的 <code>toArray(T[] array)</code>，传入的是类型完全一致、长度为 0 的空数组。</p>
</blockquote>
<hr>
<h4 id="数组转集合"><a href="#数组转集合" class="headerlink" title="数组转集合"></a>数组转集合</h4><blockquote>
<p>使用工具类 <code>Arrays.asList()</code> 把数组转换成集合时，不能使用其修改集合相关的方法， 它的 <code>add/remove/clear</code> 方法会抛出 <code>UnsupportedOperationException</code> 异常。</p>
</blockquote>
<p><strong>注意事项</strong></p>
<p><strong>1、<code>Arrays.asList()</code>是泛型方法，传递的数组必须是对象数组，而不是基本类型。</strong></p>
<p>当传入一个原生数据类型数组时，<code>Arrays.asList()</code> 的真正得到的参数就不是数组中的元素，而是数组对象本身！此时 <code>List</code> 的唯一元素就是这个数组，这也就解释了上面的代码。</p>
<p>使用包装类型数组就可以解决这个问题。</p>
<p><strong>2、使用集合的修改方法: <code>add()</code>、<code>remove()</code>、<code>clear()</code>会抛出异常。</strong></p>
<hr>
<h2 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h2><h3 id="ArrayList源码-amp-扩容机制分析"><a href="#ArrayList源码-amp-扩容机制分析" class="headerlink" title="ArrayList源码&amp;扩容机制分析"></a>ArrayList源码&amp;扩容机制分析</h3><h4 id="介绍-2"><a href="#介绍-2" class="headerlink" title="介绍"></a>介绍</h4><p><code>ArrayList</code>继承于 <strong><code>AbstractList</code></strong> ，实现了 <strong><code>List</code></strong>, <strong><code>RandomAccess</code></strong>, <strong><code>Cloneable</code></strong>, <strong><code>java.io.Serializable</code></strong> 这些接口。</p>
<ul>
<li><code>RandomAccess</code> 是一个标志接口，表明实现这个接口的 List 集合是支持<strong>快速随机访问</strong>的。在 <code>ArrayList</code> 中，我们即可以通过元素的序号快速获取元素对象，这就是快速随机访问。</li>
<li><code>ArrayList</code> 实现了 <strong><code>Cloneable</code> 接口</strong> ，即覆盖了函数<code>clone()</code>，能被克隆。</li>
<li><code>ArrayList</code> 实现了 <code>java.io.Serializable</code>接口，这意味着<code>ArrayList</code>支持序列化，能通过序列化去传输。</li>
</ul>
<h5 id="Arraylist-与-LinkedList-区别"><a href="#Arraylist-与-LinkedList-区别" class="headerlink" title="Arraylist 与 LinkedList 区别?"></a>Arraylist 与 LinkedList 区别?</h5><ol>
<li><strong>是否保证线程安全：</strong> <code>ArrayList</code> 和 <code>LinkedList</code> 都是不同步的，也就是不保证线程安全；</li>
<li><strong>底层数据结构：</strong> <code>Arraylist</code> 底层使用的是 <strong><code>Object</code> 数组</strong>；<code>LinkedList</code> 底层使用的是 <strong>双向链表</strong> 数据结构（JDK1.6 之前为循环链表，JDK1.7 取消了循环。注意双向链表和双向循环链表的区别，下面有介绍到！）</li>
<li><strong>插入和删除是否受元素位置的影响：</strong> ① <strong><code>ArrayList</code> 采用数组存储，所以插入和删除元素的时间复杂度受元素位置的影响。</strong> 比如：执行<code>add(E e)</code>方法的时候， <code>ArrayList</code> 会默认在将指定的元素追加到此列表的末尾，这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话（<code>add(int index, E element)</code>）时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位&#x2F;向前移一位的操作。 ② <strong><code>LinkedList</code> 采用链表存储，所以对于<code>add(E e)</code>方法的插入，删除元素时间复杂度不受元素位置的影响，近似 O(1)，如果是要在指定位置<code>i</code>插入和删除元素的话（<code>(add(int index, E element)</code>） 时间复杂度近似为<code>o(n))</code>因为需要先移动到指定位置再插入。</strong></li>
<li><strong>是否支持快速随机访问：</strong> <code>LinkedList</code> 不支持高效的随机元素访问，而 <code>ArrayList</code> 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于<code>get(int index)</code>方法)。</li>
<li><strong>内存空间占用：</strong> <code>ArrayList</code> 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间，而 <code>LinkedList</code> 的空间花费则体现在它的每一个元素都需要消耗比 <code>ArrayList</code> 更多的空间（因为要存放直接后继和直接前驱以及数据）。</li>
</ol>
<h4 id="赋值，浅拷贝，深拷贝"><a href="#赋值，浅拷贝，深拷贝" class="headerlink" title="赋值，浅拷贝，深拷贝"></a>赋值，浅拷贝，深拷贝</h4><p><strong>深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的</strong>。</p>
<p><img src="G:\GitDocument\Study\images\面试.assets\image-20230126114609538.png" alt="image-20230126114609538"></p>
<p><strong>浅拷贝只复制指向某个对象的指针，而不复制对象本身，新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象，新对象跟原对象不共享内存，修改新对象不会改到原对象。</strong></p>
<p><img src="G:\GitDocument\Study\images\面试.assets\image-20230126114539224.png" alt="image-20230126114539224"></p>
<hr>
<h3 id="HashMap源码-amp-底层数据结构分析"><a href="#HashMap源码-amp-底层数据结构分析" class="headerlink" title="HashMap源码&amp;底层数据结构分析"></a>HashMap源码&amp;底层数据结构分析</h3><hr>
<h3 id="ConcurrentHashMap源码-amp-底层数据结构分析"><a href="#ConcurrentHashMap源码-amp-底层数据结构分析" class="headerlink" title="ConcurrentHashMap源码&amp;底层数据结构分析"></a>ConcurrentHashMap源码&amp;底层数据结构分析</h3><h4 id="ConcurrentHashMap-1-7"><a href="#ConcurrentHashMap-1-7" class="headerlink" title="ConcurrentHashMap 1.7"></a>ConcurrentHashMap 1.7</h4><h5 id="1-存储结构"><a href="#1-存储结构" class="headerlink" title="1.存储结构"></a>1.存储结构</h5><p><img src="G:\GitDocument\Study\images\面试.assets\image-20230126152242606.png" alt="image-20230126152242606"></p>
<p><code>ConcurrnetHashMap</code> 由很多个 <code>Segment</code> 组合，而每一个 <code>Segment</code> 是一个类似于 <code>HashMap</code> 的结构，所以每一个 <code>HashMap</code> 的内部可以进行扩容。但是 <code>Segment</code> 的个数一旦<strong>初始化就不能改变</strong>，默认 <code>Segment</code> 的个数是 16 个，你也可以认为 <code>ConcurrentHashMap</code> 默认支持最多 16 个线程并发。</p>
<h4 id="ConcurrentHashMap-1-8"><a href="#ConcurrentHashMap-1-8" class="headerlink" title="ConcurrentHashMap 1.8"></a>ConcurrentHashMap 1.8</h4><h5 id="1-存储结构-1"><a href="#1-存储结构-1" class="headerlink" title="1. 存储结构"></a>1. 存储结构</h5><p><img src="G:\GitDocument\Study\images\面试.assets\image-20230126154455693.png" alt="image-20230126154455693"></p>
<p> Java8 的 ConcurrentHashMap 相对于 Java7 来说变化比较大，不再是之前的 <strong>Segment 数组 + HashEntry 数组 + 链表</strong>，而是 <strong>Node 数组 + 链表 &#x2F; 红黑树</strong>。当冲突链表达到一定长度时，链表会转换成红黑树。</p>
<h4 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h4><p>Java7 中 <code>ConcurrentHashMap</code> 使用的分段锁，也就是每一个 Segment 上同时只有一个线程可以操作，每一个 <code>Segment</code> 都是一个类似 <code>HashMap</code> 数组的结构，它可以扩容，它的冲突会转化为链表。但是 <code>Segment</code> 的个数一但初始化就不能改变。</p>
<p>Java8 中的 <code>ConcurrentHashMap</code> 使用的 <code>Synchronized</code> 锁加 CAS 的机制。结构也由 Java7 中的 <strong><code>Segment</code> 数组 + <code>HashEntry</code> 数组 + 链表</strong> 进化成了 <strong>Node 数组 + 链表 &#x2F; 红黑树</strong>，Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树，在冲突小于一定数量时又退回链表。</p>
<p>有些同学可能对 <code>Synchronized</code> 的性能存在疑问，其实 <code>Synchronized</code> 锁自从引入锁升级策略后，性能不再是问题，有兴趣的同学可以自己了解下 <code>Synchronized</code> 的<strong>锁升级</strong></p>
<hr>
<h2 id="I-x2F-O-1"><a href="#I-x2F-O-1" class="headerlink" title="I&#x2F;O"></a>I&#x2F;O</h2><h3 id="Java-IO基础知识总结"><a href="#Java-IO基础知识总结" class="headerlink" title="Java  IO基础知识总结"></a>Java  IO基础知识总结</h3><h5 id="随机访问流"><a href="#随机访问流" class="headerlink" title="随机访问流"></a>随机访问流</h5><p>这里要介绍的随机访问流指的是支持随意跳转到文件的任意位置进行读写的 <code>RandomAccessFile</code> 。</p>
<p><code>RandomAccessFile</code> 中有一个文件指针用来表示下一个将要被写入或者读取的字节所处的位置。我们可以通过 <code>RandomAccessFile</code> 的 <code>seek(long pos)</code> 方法来设置文件指针的偏移量（距文件开头 <code>pos</code> 个字节处）。如果想要获取文件指针当前的位置的话，可以使用 <code>getFilePointer()</code> 方法。</p>
<p><code>RandomAccessFile</code> 比较常见的一个应用就是实现大文件的 <strong>断点续传</strong> 。何谓断点续传？简单来说就是上传文件中途暂停或失败（比如遇到网络问题）之后，不需要重新上传，只需要上传那些未成功上传的文件分片即可。分片（先将文件切分成多个文件分片）上传是断点续传的基础。</p>
<h3 id="Java-IO设计模式总结"><a href="#Java-IO设计模式总结" class="headerlink" title="Java IO设计模式总结"></a>Java IO设计模式总结</h3><h4 id="装饰器模式"><a href="#装饰器模式" class="headerlink" title="装饰器模式"></a>装饰器模式</h4><p>装饰器模式通过组合替代继承来扩展原始类的功能，在一些继承关系比较复杂的场景（IO 这一场景各种类的继承关系就比较复杂）更加实用。</p>
<h4 id="适配器模式"><a href="#适配器模式" class="headerlink" title="适配器模式"></a>适配器模式</h4><p><strong>适配器模式和装饰器模式有什么区别呢？</strong></p>
<p><strong>装饰器模式</strong> 更侧重于动态地增强原始类的功能，装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。并且，装饰器模式支持对原始类嵌套使用多个装饰器。</p>
<p><strong>适配器模式</strong> 更侧重于让接口不兼容而不能交互的类可以一起工作，当我们调用适配器对应的方法时，适配器内部会调用适配者类或者和适配类相关的类的方法，这个过程透明的。就比如说 <code>StreamDecoder</code> （流解码器）和<code>StreamEncoder</code>（流编码器）就是分别基于 <code>InputStream</code> 和 <code>OutputStream</code> 来获取 <code>FileChannel</code>对象并调用对应的 <code>read</code> 方法和 <code>write</code> 方法进行字节数据的读取和写入。</p>
<h4 id="工厂模式"><a href="#工厂模式" class="headerlink" title="工厂模式"></a>工厂模式</h4><p>工厂模式用于创建对象，NIO 中大量用到了工厂模式。</p>
<h4 id="观察者模式"><a href="#观察者模式" class="headerlink" title="观察者模式"></a>观察者模式</h4><p>NIO 中的文件目录监听服务使用到了观察者模式。</p>
<p>NIO 中的文件目录监听服务基于 <code>WatchService</code> 接口和 <code>Watchable</code> 接口。<code>WatchService</code> 属于观察者，<code>Watchable</code> 属于被观察者。</p>
<hr>
<h3 id="Java-IO模型详解-见Java基础下"><a href="#Java-IO模型详解-见Java基础下" class="headerlink" title="Java IO模型详解  (见Java基础下)"></a>Java IO模型详解  (见Java基础下)</h3><hr>
<h2 id="并发编程"><a href="#并发编程" class="headerlink" title="并发编程"></a>并发编程</h2><h3 id="Java并发常见面试题总结（上）"><a href="#Java并发常见面试题总结（上）" class="headerlink" title="Java并发常见面试题总结（上）"></a>Java并发常见面试题总结（上）</h3><h4 id="堆和方法区"><a href="#堆和方法区" class="headerlink" title="堆和方法区"></a>堆和方法区</h4><p>堆和方法区是所有线程共享的资源，其中堆是进程中最大的一块内存，主要用于存放新创建的对象 (几乎所有对象都在这里分配内存)，方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。</p>
<hr>
<h4 id="并发与并行的区别"><a href="#并发与并行的区别" class="headerlink" title="并发与并行的区别"></a>并发与并行的区别</h4><ul>
<li><strong>并发</strong>：两个及两个以上的作业在同一 <strong>时间段</strong> 内执行。</li>
<li><strong>并行</strong>：两个及两个以上的作业在同一 <strong>时刻</strong> 执行。</li>
</ul>
<hr>
<h4 id="同步和异步的区别"><a href="#同步和异步的区别" class="headerlink" title="同步和异步的区别"></a>同步和异步的区别</h4><ul>
<li><strong>同步</strong> ： 发出一个调用之后，在没有得到结果之前， 该调用就不可以返回，一直等待。</li>
<li><strong>异步</strong> ：调用在发出之后，不用等待返回结果，该调用直接返回。</li>
</ul>
<hr>
<h4 id="线程的生命周期和状态"><a href="#线程的生命周期和状态" class="headerlink" title="线程的生命周期和状态"></a>线程的生命周期和状态</h4><h5 id="6-种不同状态"><a href="#6-种不同状态" class="headerlink" title="6 种不同状态"></a>6 种不同状态</h5><ul>
<li>NEW: 初始状态，线程被创建出来但没有被调用 <code>start()</code> 。</li>
<li>RUNNABLE: 运行状态，线程被调用了 <code>start()</code>等待运行的状态。</li>
<li>BLOCKED ：阻塞状态，需要等待锁释放。</li>
<li>WAITING：等待状态，表示该线程需要等待其他线程做出一些特定动作（通知或中断）。</li>
<li>TIME_WAITING：超时等待状态，可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。</li>
<li>TERMINATED：终止状态，表示该线程已经运行完毕。</li>
</ul>
<p><img src="G:\GitDocument\Study\images\面试.assets\image-20230129111326493.png" alt="image-20230129111326493"></p>
<blockquote>
<p>在操作系统层面，线程有 READY 和 RUNNING 状态；而在 JVM 层面，只能看到 RUNNABLE 状态，所以 Java 系统一般将这两个状态统称为 <strong>RUNNABLE（运行中）</strong> 状态 。</p>
<p><strong>为什么 JVM 没有区分这两种状态呢？</strong>  现在的时分（time-sharing）多任务（multi-task）操作系统架构通常都是用所谓的“时间分片（time quantum or time slice）”方式进行抢占式（preemptive）轮转调度（round-robin 式）。这个时间分片通常是很小的，一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间（此时处于 running 状态），也即大概只有 0.01 秒这一量级，时间片用后就要被切换下来放入调度队列的末尾等待再次调度。（也即回到 ready 状态）。线程切换的如此之快，区分这两种状态就没什么意义了。</p>
</blockquote>
<hr>
<h4 id="上下文切换"><a href="#上下文切换" class="headerlink" title="上下文切换"></a>上下文切换</h4><p>线程切换意味着需要保存当前线程的上下文，留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 <strong>上下文切换</strong>。</p>
<hr>
<h4 id="预防和避免线程死锁"><a href="#预防和避免线程死锁" class="headerlink" title="预防和避免线程死锁"></a>预防和避免线程死锁</h4><p><strong>如何预防死锁？</strong> 破坏死锁的产生的必要条件即可：</p>
<ol>
<li><strong>破坏请求与保持条件</strong> ：一次性申请所有的资源。</li>
<li><strong>破坏不剥夺条件</strong> ：占用部分资源的线程进一步申请其他资源时，如果申请不到，可以主动释放它占有的资源。</li>
<li><strong>破坏循环等待条件</strong> ：靠按序申请资源来预防。按某一顺序申请资源，释放资源则反序释放。破坏循环等待条件。</li>
</ol>
<p><strong>如何避免死锁？</strong></p>
<p>避免死锁就是在资源分配时，借助于算法（比如银行家算法）对资源分配进行计算评估，使其进入安全状态。</p>
<hr>
<h4 id="可以直接调用-Thread-类的-run-方法吗？"><a href="#可以直接调用-Thread-类的-run-方法吗？" class="headerlink" title="可以直接调用 Thread 类的 run 方法吗？"></a>可以直接调用 Thread 类的 run 方法吗？</h4><p>new 一个 <code>Thread</code>，线程进入了新建状态。调用 <code>start()</code>方法，会启动一个线程并使线程进入了就绪状态，当分配到时间片后就可以开始运行了。 <code>start()</code> 会执行线程的相应准备工作，然后自动执行 <code>run()</code> 方法的内容，这是真正的多线程工作。 但是，直接执行 <code>run()</code> 方法，会把 <code>run()</code> 方法当成一个 main 线程下的普通方法去执行，并不会在某个线程中执行它，所以这并不是多线程工作。</p>
<p><strong>总结： 调用 <code>start()</code> 方法方可启动线程并使线程进入就绪状态，直接执行 <code>run()</code> 方法的话不会以多线程的方式执行。</strong></p>
<hr>
<h3 id="Java-并发常见面试题总结（中）"><a href="#Java-并发常见面试题总结（中）" class="headerlink" title="Java 并发常见面试题总结（中）"></a>Java 并发常见面试题总结（中）</h3><h4 id="JMM-Java-Memory-Model"><a href="#JMM-Java-Memory-Model" class="headerlink" title="JMM(Java Memory Model)"></a>JMM(Java Memory Model)</h4><hr>
<h4 id="volatile-关键字"><a href="#volatile-关键字" class="headerlink" title="volatile 关键字"></a>volatile 关键字</h4><h5 id="如何保证变量的可见性？"><a href="#如何保证变量的可见性？" class="headerlink" title="如何保证变量的可见性？"></a>如何保证变量的可见性？</h5><p>在 Java 中，<code>volatile</code> 关键字可以保证变量的可见性，如果我们将变量声明为 <strong><code>volatile</code></strong> ，这就指示 JVM，这个变量是共享且不稳定的，每次使用它都到主存中进行读取。</p>
<p><code>volatile</code> 关键字能保证数据的可见性，但不能保证数据的原子性。<code>synchronized</code> 关键字两者都能保证。</p>
<hr>
<h4 id="synchronized-关键字"><a href="#synchronized-关键字" class="headerlink" title="synchronized 关键字"></a>synchronized 关键字</h4><h5 id="使用-synchronized"><a href="#使用-synchronized" class="headerlink" title="使用 synchronized"></a>使用 synchronized</h5><ol>
<li><p><strong>修饰实例方法</strong> （锁当前对象实例）</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">synchronized void method() &#123;</span><br><span class="line">    //业务代码</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>


</li>
<li><p><strong>修饰静态方法</strong> （锁当前类）</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">synchronized static void method() &#123;</span><br><span class="line">    //业务代码</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>


</li>
<li><p><strong>修饰代码块</strong>（锁指定对象&#x2F;类）</p>
<p>对括号里指定的对象&#x2F;类加锁：</p>
<ul>
<li><code>synchronized(object)</code> 表示进入同步代码库前要获得 <strong>给定对象的锁</strong>。</li>
<li><code>synchronized(类.class)</code> 表示进入同步代码前要获得 <strong>给定 Class 的锁</strong></li>
</ul>
</li>
</ol>
<hr>
<h5 id="构造方法可以用-synchronized-修饰么？"><a href="#构造方法可以用-synchronized-修饰么？" class="headerlink" title="构造方法可以用 synchronized 修饰么？"></a>构造方法可以用 synchronized 修饰么？</h5><p>先说结论：<strong>构造方法不能使用 synchronized 关键字修饰。</strong></p>
<p>构造方法本身就属于线程安全的，不存在同步的构造方法一说</p>
<hr>
<h5 id="synchronized-底层原理"><a href="#synchronized-底层原理" class="headerlink" title="synchronized 底层原理"></a>synchronized 底层原理</h5><p><code>synchronized</code> &#x3D;&#x3D;同步语句块&#x3D;&#x3D;的实现使用的是 <code>monitorenter</code> 和 <code>monitorexit</code> 指令，其中 <code>monitorenter</code> 指令指向同步代码块的开始位置，<code>monitorexit</code> 指令则指明同步代码块的结束位置。</p>
<p><code>synchronized</code> &#x3D;&#x3D;修饰的方法&#x3D;&#x3D;并没有 <code>monitorenter</code> 指令和 <code>monitorexit</code> 指令，取得代之的确实是 <code>ACC_SYNCHRONIZED</code> 标识，该标识指明了该方法是一个同步方法。JVM 通过该 <code>ACC_SYNCHRONIZED</code> 访问标志来辨别一个方法是否声明为同步方法，从而执行相应的同步调用。</p>
<p><img src="https://oss.javaguide.cn/github/javaguide/java/concurrent/synchronized-get-lock-code-block.png" alt="执行 monitorenter 获取锁"></p>
<hr>
<h5 id="JDK1-6-之后的-synchronized-底层做了哪些优化？"><a href="#JDK1-6-之后的-synchronized-底层做了哪些优化？" class="headerlink" title="JDK1.6 之后的 synchronized 底层做了哪些优化？"></a>JDK1.6 之后的 synchronized 底层做了哪些优化？</h5><p>锁主要存在四种状态，依次是：无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态，他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级，这种策略是为了提高获得锁和释放锁的效率。</p>
<hr>
<h5 id="synchronized-和-volatile-有什么区别？"><a href="#synchronized-和-volatile-有什么区别？" class="headerlink" title="synchronized 和 volatile 有什么区别？"></a>synchronized 和 volatile 有什么区别？</h5><p><code>synchronized</code> 关键字和 <code>volatile</code> 关键字是两个互补的存在，而不是对立的存在！</p>
<ul>
<li><code>volatile</code> 关键字是线程同步的轻量级实现，所以 <code>volatile</code>性能肯定比<code>synchronized</code>关键字要好 。但是 <code>volatile</code> 关键字只能用于变量而 <code>synchronized</code> 关键字可以修饰方法以及代码块 。</li>
<li><code>volatile</code> 关键字能保证数据的可见性，但不能保证数据的原子性。<code>synchronized</code> 关键字两者都能保证。</li>
<li><code>volatile</code>关键字主要用于解决变量在多个线程之间的可见性，而 <code>synchronized</code> 关键字解决的是多个线程之间访问资源的同步性。</li>
</ul>
<hr>
<h5 id="synchronized-和-ReentrantLock-有什么区别？"><a href="#synchronized-和-ReentrantLock-有什么区别？" class="headerlink" title="synchronized 和 ReentrantLock 有什么区别？"></a>synchronized 和 ReentrantLock 有什么区别？</h5><h6 id="两者都是可重入锁"><a href="#两者都是可重入锁" class="headerlink" title="两者都是可重入锁"></a>两者都是可重入锁</h6><p><strong>“可重入锁”</strong> 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁，此时这个对象锁还没有释放，当其再次想要获取这个对象的锁的时候还是可以获取的，如果是不可重入锁的话，就会造成死锁。同一个线程每次获取锁，锁的计数器都自增 1，所以要等到锁的计数器下降为 0 时才能释放锁。</p>
<p><strong>JDK 提供的所有现成的 <code>Lock</code> 实现类，包括 <code>synchronized</code> 关键字锁都是可重入的。</strong></p>
<hr>
<h5 id="synchronized-依赖于-JVM-而-ReentrantLock-依赖于-API"><a href="#synchronized-依赖于-JVM-而-ReentrantLock-依赖于-API" class="headerlink" title="synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API"></a>synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API</h5><hr>
<h5 id="ReentrantLock-比-synchronized-增加了一些高级功能"><a href="#ReentrantLock-比-synchronized-增加了一些高级功能" class="headerlink" title="ReentrantLock 比 synchronized 增加了一些高级功能"></a>ReentrantLock 比 synchronized 增加了一些高级功能</h5><ul>
<li><strong>等待可中断</strong> : <code>ReentrantLock</code>提供了一种能够中断等待锁的线程的机制，通过 <code>lock.lockInterruptibly()</code> 来实现这个机制。也就是说正在等待的线程可以选择放弃等待，改为处理其他事情。</li>
<li><strong>可实现公平锁</strong> : <code>ReentrantLock</code>可以指定是公平锁还是非公平锁。而<code>synchronized</code>只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。<code>ReentrantLock</code>默认情况是非公平的，可以通过 <code>ReentrantLock</code>类的<code>ReentrantLock(boolean fair)</code>构造方法来制定是否是公平的。</li>
<li><strong>可实现选择性通知（锁可以绑定多个条件）</strong>: <code>synchronized</code>关键字与<code>wait()</code>和<code>notify()</code>&#x2F;<code>notifyAll()</code>方法相结合可以实现等待&#x2F;通知机制。<code>ReentrantLock</code>类当然也可以实现，但是需要借助于<code>Condition</code>接口与<code>newCondition()</code>方法。</li>
</ul>
<hr>
<p><code>Condition</code>接口的补充：</p>
<p><strong>线程对象可以注册在指定的<code>Condition</code>中，从而可以有选择性的进行线程通知，在调度线程上更加灵活。 在使用<code>notify()/notifyAll()</code>方法进行通知时，被通知的线程是由 JVM 选择的，用<code>ReentrantLock</code>类结合<code>Condition</code>实例可以实现“选择性通知”</strong></p>
<hr>
<h4 id="ThreadLocal"><a href="#ThreadLocal" class="headerlink" title="ThreadLocal"></a>ThreadLocal</h4><h5 id="ThreadLocal-有什么用"><a href="#ThreadLocal-有什么用" class="headerlink" title="ThreadLocal 有什么用"></a>ThreadLocal 有什么用</h5><p><strong><code>ThreadLocal</code>类主要解决的就是让每个线程绑定自己的值，可以将<code>ThreadLocal</code>类形象的比喻成存放数据的盒子，盒子中可以存储每个线程的私有数据。</strong></p>
<p><strong>最终的变量是放在了当前线程的 <code>ThreadLocalMap</code> 中，并不是存在 <code>ThreadLocal</code> 上，<code>ThreadLocal</code> 可以理解为只是<code>ThreadLocalMap</code>的封装，传递了变量值。</strong></p>
<p><strong>每个<code>Thread</code>中都具备一个<code>ThreadLocalMap</code>，而<code>ThreadLocalMap</code>可以存储以<code>ThreadLocal</code>为 key ，Object 对象为 value 的键值对。</strong></p>
<p><img src="G:\GitDocument\Study\images\面试.assets\image-20230131152857687.png" alt="image-20230131152857687"></p>
<hr>
<h5 id="ThreadLocal-内存泄露问题是怎么导致的？"><a href="#ThreadLocal-内存泄露问题是怎么导致的？" class="headerlink" title="ThreadLocal 内存泄露问题是怎么导致的？"></a>ThreadLocal 内存泄露问题是怎么导致的？</h5><p><code>ThreadLocalMap</code> 中使用的 key 为 <code>ThreadLocal</code> 的弱引用，而 value 是强引用。所以，如果 <code>ThreadLocal</code> 没有被外部强引用的情况下，在垃圾回收的时候，key 会被清理掉，而 value 不会被清理掉。</p>
<hr>
<h3 id="Java-并发常见面试题总结（下）"><a href="#Java-并发常见面试题总结（下）" class="headerlink" title="Java 并发常见面试题总结（下）"></a>Java 并发常见面试题总结（下）</h3><h4 id="线程池"><a href="#线程池" class="headerlink" title="线程池"></a>线程池</h4><h5 id="为什么要用线程池？"><a href="#为什么要用线程池？" class="headerlink" title="为什么要用线程池？"></a>为什么要用线程池？</h5><p><strong>池化技术想必大家已经屡见不鲜了，线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗，提高对资源的利用率。</strong></p>
<h6 id="使用线程池的好处："><a href="#使用线程池的好处：" class="headerlink" title="使用线程池的好处："></a><strong>使用线程池的好处</strong>：</h6><p><strong>降低资源消耗</strong>。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。</p>
<p><strong>提高响应速度</strong>。当任务到达时，任务可以不需要等到线程创建就能立即执行。</p>
<p><strong>提高线程的可管理性</strong>。线程是稀缺资源，如果无限制的创建，不仅会消耗系统资源，还会降低系统的稳定性，使用线程池可以进行统一的分配，调优和监控。</p>
<hr>
<h5 id="线程池原理是什么？"><a href="#线程池原理是什么？" class="headerlink" title="线程池原理是什么？"></a>线程池原理是什么？</h5><p><img src="G:\GitDocument\Study\images\面试.assets\image-20230131163042871.png" alt="image-20230131163042871"></p>
<h5 id="如何设定线程池的大小？"><a href="#如何设定线程池的大小？" class="headerlink" title="如何设定线程池的大小？"></a>如何设定线程池的大小？</h5><p><strong>如果我们设置的线程池数量太小的话，如果同一时间有大量任务&#x2F;请求需要处理，可能会导致大量的请求&#x2F;任务在任务队列中排队等待执行，甚至会出现任务队列满了之后任务&#x2F;请求无法处理的情况，或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的！ CPU 根本没有得到充分利用。</strong></p>
<p><strong>但是，如果我们设置线程数量太大，大量线程可能会同时在争取 CPU 资源，这样会导致大量的上下文切换，从而增加线程的执行时间，影响了整体执行效率。</strong></p>
<p>公式：</p>
<ul>
<li><strong>CPU 密集型任务(N+1)</strong></li>
<li><strong>I&#x2F;O 密集型任务(2N)</strong>:线程在处理 I&#x2F;O 的时间段内不会占用 CPU 来处理</li>
</ul>
<p><strong>如何判断是 CPU 密集任务还是 IO 密集任务？</strong></p>
<p>CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。</p>
<p> IO 密集型涉及到网络读取，文件读取</p>
<hr>
<h4 id="Atomic-原子类"><a href="#Atomic-原子类" class="headerlink" title="Atomic 原子类"></a>Atomic 原子类</h4><h5 id="AQS"><a href="#AQS" class="headerlink" title="AQS"></a>AQS</h5><h6 id="AQS-是什么？"><a href="#AQS-是什么？" class="headerlink" title="AQS 是什么？"></a>AQS 是什么？</h6><p>AQS 的全称为 <code>AbstractQueuedSynchronizer</code> ，翻译过来的意思就是抽象队列同步器。</p>
<h6 id="AQS-的原理是什么？"><a href="#AQS-的原理是什么？" class="headerlink" title="AQS 的原理是什么？"></a>AQS 的原理是什么？</h6><p>锁分配的机制：<strong>CLH 队列锁</strong> ，即将暂时获取不到锁的线程加入到队列中</p>
<p>CLH(Craig,Landin,and Hagersten) 队列是一个虚拟的双向队列（虚拟的双向队列即不存在队列实例，仅存在结点之间的关联关系）。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点（Node）来实现锁的分配。在 CLH 同步队列中，一个节点表示一个线程，它保存着线程的引用（thread）、 当前节点在队列中的状态（waitStatus）、前驱节点（prev）、后继节点（next）。</p>
<hr>
<h5 id="Semaphore"><a href="#Semaphore" class="headerlink" title="Semaphore"></a>Semaphore</h5><h6 id="Semaphore-有什么用？"><a href="#Semaphore-有什么用？" class="headerlink" title="Semaphore 有什么用？"></a>Semaphore 有什么用？</h6><p><code>synchronized</code> 和 <code>ReentrantLock</code> 都是一次只允许一个线程访问某个资源，而<code>Semaphore</code>(信号量)可以用来控制同时访问特定资源的线程数量。</p>
<p>当初始的资源个数为 1 的时候，<code>Semaphore</code> 退化为排他锁。</p>
<p><code>Semaphore</code> 有两种模式：</p>
<ul>
<li><strong>公平模式：</strong> 调用 <code>acquire()</code> 方法的顺序就是获取许可证的顺序，遵循 FIFO；</li>
<li><strong>非公平模式：</strong> 抢占式的。</li>
</ul>
<hr>
<h6 id="Semaphore-的原理是什么？"><a href="#Semaphore-的原理是什么？" class="headerlink" title="Semaphore 的原理是什么？"></a>Semaphore 的原理是什么？</h6><p>调用<code>semaphore.acquire()</code> ，线程尝试获取许可证，如果 <code>state &gt;= 0</code> 的话，则表示可以获取成功。如果获取成功的话，使用 CAS 操作去修改 <code>state</code> 的值 <code>state=state-1</code>。如果 <code>state&lt;0</code> 的话，则表示许可证数量不足。此时会创建一个 Node 节点加入阻塞队列，挂起当前线程。</p>
<hr>
<h5 id="CountDownLatch"><a href="#CountDownLatch" class="headerlink" title="CountDownLatch"></a>CountDownLatch</h5><h6 id="CountDownLatch-有什么用？"><a href="#CountDownLatch-有什么用？" class="headerlink" title="CountDownLatch 有什么用？"></a>CountDownLatch 有什么用？</h6><p><code>CountDownLatch</code> 允许 <code>count</code> 个线程阻塞在一个地方，直至所有线程的任务都执行完毕。</p>
<p><code>CountDownLatch</code> 是一次性的，计数器的值只能在构造方法中初始化一次，之后没有任何机制再次对其设置值，当 <code>CountDownLatch</code> 使用完毕后，它不能再次被使用。</p>
<hr>
<h6 id="CountDownLatch-的原理是什么？"><a href="#CountDownLatch-的原理是什么？" class="headerlink" title="CountDownLatch 的原理是什么？"></a>CountDownLatch 的原理是什么？</h6><p><code>CountDownLatch</code> 是共享锁的一种实现,它默认构造 AQS 的 <code>state</code> 值为 <code>count</code>。<code>CountDownLatch</code> 会自旋 CAS 判断 <code>state == 0</code>，如果 <code>state == 0</code> 的话，就会释放所有等待的线程，<code>await()</code> 方法之后的语句得到执行。</p>
<h6 id="用过-CountDownLatch-么？什么场景下用的？"><a href="#用过-CountDownLatch-么？什么场景下用的？" class="headerlink" title="用过 CountDownLatch 么？什么场景下用的？"></a>用过 CountDownLatch 么？什么场景下用的？</h6><h5 id="CyclicBarrier"><a href="#CyclicBarrier" class="headerlink" title="CyclicBarrier"></a>CyclicBarrier</h5><h6 id="CyclicBarrier-有什么用？"><a href="#CyclicBarrier-有什么用？" class="headerlink" title="CyclicBarrier 有什么用？"></a>CyclicBarrier 有什么用？</h6><p><code>CyclicBarrier</code> 和 <code>CountDownLatch</code> 非常类似，它也可以实现线程间的技术等待，但是它的功能比 <code>CountDownLatch</code> 更加复杂和强大。主要应用场景和 <code>CountDownLatch</code> 类似。</p>
<hr>
<h6 id="CyclicBarrier-的原理是什么？"><a href="#CyclicBarrier-的原理是什么？" class="headerlink" title="CyclicBarrier 的原理是什么？"></a>CyclicBarrier 的原理是什么？</h6><hr>
<h3 id="重要知识点-1"><a href="#重要知识点-1" class="headerlink" title="重要知识点"></a>重要知识点</h3><h4 id="JMM（Java-内存模型）详解"><a href="#JMM（Java-内存模型）详解" class="headerlink" title="JMM（Java 内存模型）详解"></a>JMM（Java 内存模型）详解</h4><p> <strong>CPU 缓存模型和指令重排序</strong></p>
<h5 id="CPU-缓存模型"><a href="#CPU-缓存模型" class="headerlink" title="CPU 缓存模型"></a>CPU 缓存模型</h5><p><strong>CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。</strong>甚至可以把 <strong>内存看作外存的高速缓存</strong></p>
<p>总结：<strong>CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题，内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。</strong></p>
<p><strong>CPU Cache 的工作方式：</strong> 先复制一份数据到 CPU Cache 中，当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据，当运算完成后，再将运算得到的数据写回 Main Memory 中。存在 <strong>内存缓存不一致性的问题</strong></p>
<p><strong>CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议（比如 [MESI 协议）或者其他手段来解决。</strong></p>
<hr>
<h5 id="指令重排序"><a href="#指令重排序" class="headerlink" title="指令重排序"></a>指令重排序</h5><p><strong>什么是指令重排序？</strong> 简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。</p>
<p><strong>编译器优化重排</strong> ：编译器（包括 JVM、JIT 编译器等）在不改变单线程程序语义的前提下，重新安排语句的执行顺序。</p>
<p><strong>指令并行重排</strong> ：现代处理器采用了指令级并行技术(Instruction-Level Parallelism，ILP)来将多条指令重叠执行。如果不存在数据依赖性，处理器可以改变语句对应机器指令的执行顺序。</p>
<p><strong>指令重排序可以保证串行语义一致，但是没有义务保证多线程间的语义也一致</strong> ，所以在多线程下，指令重排序可能会导致一些问题。</p>
<hr>
<h4 id="JMM-Java-Memory-Model-1"><a href="#JMM-Java-Memory-Model-1" class="headerlink" title="JMM(Java Memory Model)"></a>JMM(Java Memory Model)</h4><h5 id="什么是-JMM？为什么需要-JMM？"><a href="#什么是-JMM？为什么需要-JMM？" class="headerlink" title="什么是 JMM？为什么需要 JMM？"></a>什么是 JMM？为什么需要 JMM？</h5><p>定义了一些规范来解决像 CPU 多级缓存和指令重排这类设计可能会导致程序运行出现的一些问题</p>
<h5 id="JMM-是如何抽象线程和主内存之间的关系？"><a href="#JMM-是如何抽象线程和主内存之间的关系？" class="headerlink" title="JMM 是如何抽象线程和主内存之间的关系？"></a>JMM 是如何抽象线程和主内存之间的关系？</h5><h6 id="什么是主内存？什么是本地内存？"><a href="#什么是主内存？什么是本地内存？" class="headerlink" title="什么是主内存？什么是本地内存？"></a><strong>什么是主内存？什么是本地内存？</strong></h6><p><strong>主内存</strong> ：所有线程创建的实例对象都存放在主内存中，不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)</p>
<p><strong>本地内存</strong> ：每个线程都有一个私有的本地内存来存储共享变量的副本，并且，每个线程只能访问自己的本地内存，无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念，存储了主内存中的共享变量副本。</p>
<p><img src="G:\GitDocument\Study\images\面试.assets\image-20230201195258399.png" alt="image-20230201195258399"></p>
<hr>
<h5 id="Java-内存区域和-JMM-有何区别？"><a href="#Java-内存区域和-JMM-有何区别？" class="headerlink" title="Java 内存区域和 JMM 有何区别？"></a>Java 内存区域和 JMM 有何区别？</h5><p><strong>JVM 内存结构</strong>和 Java 虚拟机的运行时区域相关，定义了 JVM 在运行时如何分区存储程序数据，就比如说堆主要用于存放对象实例。</p>
<p><strong>Java 内存模型</strong>和 Java 的并发编程相关，抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中，规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范，其主要目的是为了简化多线程编程，增强程序可移植性的。</p>
<hr>
<h6 id="happens-before-原则是什么？"><a href="#happens-before-原则是什么？" class="headerlink" title="happens-before 原则是什么？"></a>happens-before 原则是什么？</h6><p><strong>逻辑时钟并不度量时间本身，仅区分事件发生的前后顺序，其本质就是定义了一种 happens-before 关系。</strong></p>
<ul>
<li>为了对编译器和处理器的约束尽可能少，只要不改变程序的执行结果（单线程程序和正确执行的多线程程序），编译器和处理器怎么进行重排序优化都行。</li>
<li>对于会改变程序执行结果的重排序，JMM 要求编译器和处理器必须禁止这种重排序。</li>
</ul>
<p><img src="G:\GitDocument\Study\images\面试.assets\image-20230202151006306.png"></p>
<p>happens-before 原则表达的意义其实并不是一个操作发生在另外一个操作的前面，虽然这从程序员的角度上来说也并无大碍。更准确地来说，它更想表达的意义是前一个操作的结果对于后一个操作是可见的，无论这两个操作是否在同一个线程里。</p>
<hr>
<h5 id="总结-2"><a href="#总结-2" class="headerlink" title="总结"></a>总结</h5><ul>
<li>Java 是最早尝试提供内存模型的语言，其主要目的是为了简化多线程编程，增强程序可移植性的。</li>
<li>CPU 可以通过制定缓存一致协议（比如 <a target="_blank" rel="noopener" href="https://zh.wikipedia.org/wiki/MESI%E5%8D%8F%E8%AE%AE">MESI 协议open in new window</a>）来解决内存缓存不一致性问题。</li>
<li>为了提升执行速度&#x2F;性能，计算机在执行程序代码的时候，会对指令进行重排序。 简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。<strong>指令重排序可以保证串行语义一致，但是没有义务保证多线程间的语义也一致</strong> ，所以在多线程下，指令重排序可能会导致一些问题。</li>
<li>你可以把 JMM 看作是 Java 定义的并发编程相关的一组规范，除了抽象了线程和主内存之间的关系之外，其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范，其主要目的是为了简化多线程编程，增强程序可移植性的。</li>
<li>JSR 133 引入了 happens-before 这个概念来描述两个操作之间的内存可见性。</li>
</ul>
<hr>
<h4 id="Java-线程池详解"><a href="#Java-线程池详解" class="headerlink" title="Java 线程池详解"></a>Java 线程池详解</h4><h5 id="使用线程池的好处"><a href="#使用线程池的好处" class="headerlink" title="使用线程池的好处"></a>使用线程池的好处</h5><ul>
<li><strong>降低资源消耗</strong>。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。</li>
<li><strong>提高响应速度</strong>。当任务到达时，任务可以不需要等到线程创建就能立即执行。</li>
<li><strong>提高线程的可管理性</strong>。线程是稀缺资源，如果无限制的创建，不仅会消耗系统资源，还会降低系统的稳定性，使用线程池可以进行统一的分配，调优和监控。</li>
</ul>
<hr>
<h5 id="Executor-框架"><a href="#Executor-框架" class="headerlink" title="Executor 框架"></a>Executor 框架</h5><h5 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h5><p>有助于避免 this 逃逸问题</p>
<p>补充：this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误。</p>
<h5 id="Executor-框架结构-主要由三大部分组成"><a href="#Executor-框架结构-主要由三大部分组成" class="headerlink" title="Executor 框架结构(主要由三大部分组成)"></a>Executor 框架结构(主要由三大部分组成)</h5><h6 id="任务-Runnable-x2F-Callable"><a href="#任务-Runnable-x2F-Callable" class="headerlink" title="任务(Runnable &#x2F;Callable)"></a>任务(<code>Runnable</code> &#x2F;<code>Callable</code>)</h6><h6 id="任务的执行-Executor"><a href="#任务的执行-Executor" class="headerlink" title="任务的执行(Executor)"></a>任务的执行(<code>Executor</code>)</h6><p><strong>实际上我们需要更多关注的是 <code>ThreadPoolExecutor</code> 这个类，这个类在我们实际使用线程池的过程中，使用频率还是非常高的。</strong></p>
<h6 id="异步计算的结果-Future"><a href="#异步计算的结果-Future" class="headerlink" title="异步计算的结果(Future)"></a>异步计算的结果(<code>Future</code>)</h6><p>当我们把 <strong><code>Runnable</code>接口</strong> 或 <strong><code>Callable</code> 接口</strong> 的实现类提交给 <strong><code>ThreadPoolExecutor</code></strong> 或 <strong><code>ScheduledThreadPoolExecutor</code></strong> 执行。（调用 <code>submit()</code> 方法时会返回一个 <strong><code>FutureTask</code></strong> 对象）</p>
<h5 id="Executor-框架的使用示意图"><a href="#Executor-框架的使用示意图" class="headerlink" title="Executor 框架的使用示意图"></a>Executor 框架的使用示意图</h5><p><img src="G:\GitDocument\Study\images\面试.assets\image-20230202160427504.png" alt="image-20230202160427504"></p>
<ol>
<li><strong>主线程首先要创建实现 <code>Runnable</code> 或者 <code>Callable</code> 接口的任务对象。</strong></li>
<li><em><em>把创建完成的实现 <code>Runnable</code>&#x2F;<code>Callable</code>接口的 对象直接交给</em><code>ExecutorService</code> 执行</em>*: </li>
<li><strong>如果执行 <code>ExecutorService.submit（…）</code>，<code>ExecutorService</code> 将返回一个实现<code>Future</code>接口的对象</strong></li>
<li><strong>最后，主线程可以执行 <code>FutureTask.get()</code>方法来等待任务执行完成。主线程也可以执行 <code>FutureTask.cancel（boolean mayInterruptIfRunning）</code>来取消此任务的执行。</strong></li>
</ol>
<hr>
<h5 id="x3D-x3D-重要-x3D-x3D-ThreadPoolExecutor-类简单介绍"><a href="#x3D-x3D-重要-x3D-x3D-ThreadPoolExecutor-类简单介绍" class="headerlink" title="(&#x3D;&#x3D;重要&#x3D;&#x3D;)ThreadPoolExecutor 类简单介绍"></a>(&#x3D;&#x3D;重要&#x3D;&#x3D;)ThreadPoolExecutor 类简单介绍</h5><p><strong>线程池实现类 <code>ThreadPoolExecutor</code> 是 <code>Executor</code> 框架最核心的类。</strong></p>
<h5 id="ThreadPoolExecutor-类分析"><a href="#ThreadPoolExecutor-类分析" class="headerlink" title="ThreadPoolExecutor 类分析"></a>ThreadPoolExecutor 类分析</h5><p><strong><code>ThreadPoolExecutor</code> 3 个最重要的参数：</strong></p>
<ul>
<li><strong><code>corePoolSize</code> :</strong> 核心线程数线程数定义了最小可以同时运行的线程数量。</li>
<li><strong><code>maximumPoolSize</code> :</strong> 当队列中存放的任务达到队列容量的时候，当前可以同时运行的线程数量变为最大线程数。</li>
<li><strong><code>workQueue</code>:</strong> 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数，如果达到的话，新任务就会被存放在队列中。</li>
</ul>
<p><img src="G:\GitDocument\Study\images\面试.assets\image-20230203155940860.png" alt="image-20230203155940860"></p>
<hr>
<h6 id="推荐使用-ThreadPoolExecutor-构造函数创建线程池"><a href="#推荐使用-ThreadPoolExecutor-构造函数创建线程池" class="headerlink" title="推荐使用 ThreadPoolExecutor 构造函数创建线程池"></a>推荐使用 <code>ThreadPoolExecutor</code> 构造函数创建线程池</h6><p><strong>为什么呢？</strong></p>
<blockquote>
<p>使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销，解决资源不足的问题。如果不使用线程池，有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。</p>
</blockquote>
<p>不允许使用 <code>Executors</code> 去创建，而是通过 <code>ThreadPoolExecutor</code> 构造函数的方式，这样的处理方式让写的同学更加明确线程池的运行规则，规避资源耗尽的风险</p>
<blockquote>
<p><code>Executors</code> 返回线程池对象的弊端如下(后文会详细介绍到)：</p>
<ul>
<li><strong><code>FixedThreadPool</code> 和 <code>SingleThreadExecutor</code></strong> ： 允许请求的队列长度为 <code>Integer.MAX_VALUE</code>,可能堆积大量的请求，从而导致 OOM。</li>
<li><strong><code>CachedThreadPool</code> 和 <code>ScheduledThreadPool</code></strong> ： 允许创建的线程数量为 <code>Integer.MAX_VALUE</code> ，可能会创建大量线程，从而导致 OOM。</li>
</ul>
</blockquote>
<p><strong>方式一：通过<code>ThreadPoolExecutor</code>构造函数实现（推荐）</strong><img src="https://javaguide.cn/assets/threadpoolexecutor%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0.d54a5992.png" alt="通过构造方法实现"></p>
<p><strong>方式二：通过 <code>Executor</code> 框架的工具类 <code>Executors</code> 来实现</strong> 我们可以创建三种类型的 <code>ThreadPoolExecutor</code>：</p>
<ul>
<li><code>FixedThreadPool</code></li>
<li><code>SingleThreadExecutor</code></li>
<li>CachedThreadPool</li>
</ul>
<h5 id="ThreadPoolExecutor-使用-原理分析"><a href="#ThreadPoolExecutor-使用-原理分析" class="headerlink" title="ThreadPoolExecutor 使用+原理分析"></a>ThreadPoolExecutor 使用+原理分析</h5><h6 id="示例代码-Runnable-ThreadPoolExecutor"><a href="#示例代码-Runnable-ThreadPoolExecutor" class="headerlink" title="示例代码:Runnable+ThreadPoolExecutor"></a>示例代码:<code>Runnable</code>+<code>ThreadPoolExecutor</code></h6><h6 id="线程池原理分析"><a href="#线程池原理分析" class="headerlink" title="线程池原理分析"></a>线程池原理分析</h6><h6 id="几个常见的对比"><a href="#几个常见的对比" class="headerlink" title="几个常见的对比"></a>几个常见的对比</h6><p><code>**Runnable</code> vs <code>Callable</code>**</p>
<p><code>Callable</code>在 Java 1.5 中引入,目的就是为了来处理<code>Runnable</code>不支持的用例。**<code>Runnable</code> 接口**不会返回结果或抛出检查异常，但是 <strong><code>Callable</code> 接口</strong>可以。</p>
<p>工具类 <code>Executors</code> 可以实现将 <code>Runnable</code> 对象转换成 <code>Callable</code> 对象。</p>
<p><strong><code>execute()</code> vs <code>submit()</code></strong></p>
<p><code>execute()</code>方法用于提交不需要返回值的任务，所以无法判断任务是否被线程池执行成功与否；</p>
<p><code>submit()</code>方法用于提交需要返回值的任务。线程池会返回一个 <code>Future</code> 类型的对象，通过这个 <code>Future</code> 对象可以判断任务是否执行成功，并且可以通过 <code>Future</code> 的 <code>get()</code>方法来获取返回值，<code>get()</code>方法会阻塞当前线程直到任务完成，而使用 <code>get（long timeout，TimeUnit unit）</code>方法的话，如果在 <code>timeout</code> 时间内任务还没有执行完，就会抛出 <code>java.util.concurrent.TimeoutException</code>。</p>
<p><code>shutdown()</code>VS<code>shutdownNow()</code></p>
<ul>
<li><strong><code>shutdown（）</code></strong> :关闭线程池，线程池的状态变为 <code>SHUTDOWN</code>。线程池不再接受新任务了，但是队列里的-得执行完毕。</li>
<li><strong><code>shutdownNow（）</code></strong> :关闭线程池，线程的状态变为 <code>STOP</code>。线程池会终止当前正在运行的任务，并停止处理排队的任务并返回正在等待执行的 List。</li>
</ul>
<hr>
<p><strong><code>isTerminated()</code> VS <code>isShutdown()</code></strong></p>
<ul>
<li><strong><code>isShutDown</code></strong> 当调用 <code>shutdown()</code> 方法后返回为 true。</li>
<li><strong><code>isTerminated</code></strong> 当调用 <code>shutdown()</code> 方法后，并且所有提交的任务完成后返回为 true</li>
</ul>
<hr>
<p><strong>加餐:<code>Callable</code>+<code>ThreadPoolExecutor</code>示例代码</strong></p>
<hr>
<h5 id="几种常见的线程池详解"><a href="#几种常见的线程池详解" class="headerlink" title="几种常见的线程池详解"></a>几种常见的线程池详解</h5><h6 id="FixedThreadPool"><a href="#FixedThreadPool" class="headerlink" title="FixedThreadPool"></a>FixedThreadPool</h6><p><code>FixedThreadPool</code> 被称为可重用固定线程数的线程池。</p>
<h6 id="SingleThreadExecutor-详解"><a href="#SingleThreadExecutor-详解" class="headerlink" title="SingleThreadExecutor 详解"></a>SingleThreadExecutor 详解</h6><p><code>SingleThreadExecutor</code> 是只有一个线程的线程池。</p>
<h6 id="CachedThreadPool-详解"><a href="#CachedThreadPool-详解" class="headerlink" title="CachedThreadPool 详解"></a>CachedThreadPool 详解</h6><p><code>CachedThreadPool</code> 是一个会根据需要创建新线程的线程池。</p>
<p><code>CachedThreadPool</code>允许创建的线程数量为 <code>Integer.MAX_VALUE</code> ，可能会创建大量线程，从而导致 OOM。</p>
<hr>
<h5 id="ScheduledThreadPoolExecutor-详解"><a href="#ScheduledThreadPoolExecutor-详解" class="headerlink" title="ScheduledThreadPoolExecutor 详解"></a>ScheduledThreadPoolExecutor 详解</h5><p><strong><code>ScheduledThreadPoolExecutor</code> 主要用来在给定的延迟后运行任务，或者定期执行任务。</strong></p>
<h6 id="ScheduledThreadPoolExecutor-和-Timer-的比较："><a href="#ScheduledThreadPoolExecutor-和-Timer-的比较：" class="headerlink" title="ScheduledThreadPoolExecutor 和 Timer 的比较："></a><strong><code>ScheduledThreadPoolExecutor</code> 和 <code>Timer</code> 的比较：</strong></h6><h5 id="线程池大小确定"><a href="#线程池大小确定" class="headerlink" title="线程池大小确定"></a>线程池大小确定</h5><h4 id="Java-线程池最佳实践"><a href="#Java-线程池最佳实践" class="headerlink" title="Java 线程池最佳实践"></a>Java 线程池最佳实践</h4><h5 id="线程池知识回顾"><a href="#线程池知识回顾" class="headerlink" title="线程池知识回顾"></a>线程池知识回顾</h5><h6 id="为什么要使用线程池？"><a href="#为什么要使用线程池？" class="headerlink" title="为什么要使用线程池？"></a>为什么要使用线程池？</h6><blockquote>
<p><strong>池化技术想必大家已经屡见不鲜了，线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗，提高对资源的利用率。</strong></p>
</blockquote>
<ul>
<li><strong>降低资源消耗</strong>。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。</li>
<li><strong>提高响应速度</strong>。当任务到达时，任务可以不需要等到线程创建就能立即执行。</li>
<li><strong>提高线程的可管理性</strong>。线程是稀缺资源，如果无限制的创建，不仅会消耗系统资源，还会降低系统的稳定性，使用线程池可以进行统一的分配，调优和监控。</li>
</ul>
<hr>
<h6 id="线程池在实际项目的使用场景"><a href="#线程池在实际项目的使用场景" class="headerlink" title="线程池在实际项目的使用场景"></a>线程池在实际项目的使用场景</h6><p><strong>线程池一般用于执行多个不相关联的耗时任务，没有多线程的情况下，任务顺序执行，使用了线程池的话可让多个不相关联的任务同时执行。</strong></p>
<hr>
<h5 id="线程池最佳实践"><a href="#线程池最佳实践" class="headerlink" title="线程池最佳实践"></a>线程池最佳实践</h5><h6 id="使用-ThreadPoolExecutor-的构造函数声明线程池"><a href="#使用-ThreadPoolExecutor-的构造函数声明线程池" class="headerlink" title="使用 ThreadPoolExecutor 的构造函数声明线程池"></a>使用 <code>ThreadPoolExecutor</code> 的构造函数声明线程池</h6><p>线程池必须手动通过 <code>ThreadPoolExecutor</code> 的构造函数来声明，避免使用<code>Executors</code> 类的 <code>newFixedThreadPool</code> 和 <code>newCachedThreadPool</code> ，因为可能会有 OOM 的风险。</p>
<p>弊端：</p>
<ul>
<li><strong><code>FixedThreadPool</code> 和 <code>SingleThreadExecutor</code></strong> ： 允许请求的队列长度为 <code>Integer.MAX_VALUE</code>,可能堆积大量的请求，从而导致 OOM。</li>
<li><strong>CachedThreadPool 和 ScheduledThreadPool</strong> ： 允许创建的线程数量为 <code>Integer.MAX_VALUE</code> ，可能会创建大量线程，从而导致 OOM。</li>
</ul>
<hr>
<h6 id="监测线程池运行状态"><a href="#监测线程池运行状态" class="headerlink" title="监测线程池运行状态"></a>监测线程池运行状态</h6><p>SpringBoot 中的 Actuator 组件。</p>
<hr>
<h6 id="建议不同类别的业务用不同的线程池"><a href="#建议不同类别的业务用不同的线程池" class="headerlink" title="建议不同类别的业务用不同的线程池"></a>建议不同类别的业务用不同的线程池</h6><p>代码可能会存在死锁的情况</p>
<hr>
<h6 id="别忘记给线程池命名"><a href="#别忘记给线程池命名" class="headerlink" title="别忘记给线程池命名"></a>别忘记给线程池命名</h6><p>初始化线程池的时候需要显示命名（设置线程池名称前缀），有利于定位问题。</p>
<hr>
<h6 id="正确配置线程池参数"><a href="#正确配置线程池参数" class="headerlink" title="正确配置线程池参数"></a>正确配置线程池参数</h6><blockquote>
<ul>
<li><strong>CPU 密集型任务(N+1)：</strong> 这种任务消耗的主要是 CPU 资源，可以将线程数设置为 N（CPU 核心数）+1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断，或者其它原因导致的任务暂停而带来的影响。一旦任务暂停，CPU 就会处于空闲状态，而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。</li>
<li><strong>I&#x2F;O 密集型任务(2N)：</strong> 这种任务应用起来，系统会用大部分的时间来处理 I&#x2F;O 交互，而线程在处理 I&#x2F;O 的时间段内不会占用 CPU 来处理，这时就可以将 CPU 交出给其它线程使用。因此在 I&#x2F;O 密集型任务的应用中，我们可以多配置一些线程，具体的计算方法是 2N。</li>
</ul>
</blockquote>
<p>三个核心参数是：</p>
<ul>
<li><strong><code>corePoolSize</code> :</strong> 核心线程数线程数定义了最小可以同时运行的线程数量。</li>
<li><strong><code>maximumPoolSize</code> :</strong> 当队列中存放的任务达到队列容量的时候，当前可以同时运行的线程数量变为最大线程数。</li>
<li><strong><code>workQueue</code>:</strong> 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数，如果达到的话，新任务就会被存放在队列中。</li>
</ul>
<hr>
<h4 id="Java-常见并发容器总结"><a href="#Java-常见并发容器总结" class="headerlink" title="Java 常见并发容器总结"></a>Java 常见并发容器总结</h4><ul>
<li><strong><code>ConcurrentHashMap</code></strong> : 线程安全的 <code>HashMap</code></li>
<li><strong><code>CopyOnWriteArrayList</code></strong> : 线程安全的 <code>List</code>，在读多写少的场合性能非常好，远远好于 <code>Vector</code>。</li>
<li><strong><code>ConcurrentLinkedQueue</code></strong> : 高效的并发队列，使用链表实现。可以看做一个线程安全的 <code>LinkedList</code>，这是一个非阻塞队列。</li>
<li><strong><code>BlockingQueue</code></strong> : 这是一个接口，JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列，非常适合用于作为数据共享的通道。</li>
<li><strong><code>ConcurrentSkipListMap</code></strong> : 跳表的实现。这是一个 Map，使用跳表的数据结构进行快速查找。</li>
</ul>
<hr>
<h5 id="ConcurrentHashMap"><a href="#ConcurrentHashMap" class="headerlink" title="ConcurrentHashMap"></a>ConcurrentHashMap</h5><p>在进行读操作时(几乎)不需要加锁，而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。</p>
<hr>
<h5 id="CopyOnWriteArrayList"><a href="#CopyOnWriteArrayList" class="headerlink" title="CopyOnWriteArrayList"></a>CopyOnWriteArrayList</h5><h6 id="简介-1"><a href="#简介-1" class="headerlink" title="简介"></a>简介</h6><p><code>CopyOnWriteArrayList</code> 读取是完全不用加锁的，写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来，读操作的性能就会大幅度提升。</p>
<hr>
<h6 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h6><p><code>CopyOnWriteArrayList</code> 类的所有可变操作（add，set 等等）都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候，我并不修改原有内容，而是对原有数据进行一次复制，将修改的内容写入副本。写完之后，再将修改完的副本替换原来的数据，这样就可以保证写操作不会影响读操作了。</p>
<hr>
<h5 id="ConcurrentLinkedQueue"><a href="#ConcurrentLinkedQueue" class="headerlink" title="ConcurrentLinkedQueue"></a>ConcurrentLinkedQueue</h5><p>Java 提供的线程安全的 <code>Queue</code> 可以分为<strong>阻塞队列</strong>和<strong>非阻塞队列</strong>， <strong>阻塞队列可以通过加锁来实现，非阻塞队列可以通过 CAS 操作实现。</strong></p>
<hr>
<h5 id="BlockingQueue"><a href="#BlockingQueue" class="headerlink" title="BlockingQueue"></a>BlockingQueue</h5><h6 id="简介-2"><a href="#简介-2" class="headerlink" title="简介"></a>简介</h6><p><code>BlockingQueue</code> 提供了可阻塞的插入和移除的方法，被广泛使用在“生产者-消费者”问题中。</p>
<p> 3 个常见的 <code>BlockingQueue</code> 的实现类：<code>ArrayBlockingQueue</code>、<code>LinkedBlockingQueue</code> 、<code>PriorityBlockingQueue</code> </p>
<h6 id="ArrayBlockingQueue"><a href="#ArrayBlockingQueue" class="headerlink" title="ArrayBlockingQueue"></a>ArrayBlockingQueue</h6><p>有界队列实现类，底层采用数组来实现。默认情况下不能保证线程访问队列的公平性，所谓公平性是指严格按照线程等待的绝对时间顺序，即最先等待的线程能够最先访问到 <code>ArrayBlockingQueue</code>。如果保证公平性，通常会降低吞吐量。</p>
<p>获取公平性：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">private static ArrayBlockingQueue&lt;Integer&gt; blockingQueue = new ArrayBlockingQueue&lt;Integer&gt;(10,true);</span><br></pre></td></tr></table></figure>

<hr>
<h6 id="LinkedBlockingQueue"><a href="#LinkedBlockingQueue" class="headerlink" title="LinkedBlockingQueue"></a>LinkedBlockingQueue</h6><p> 底层基于<strong>单向链表</strong>实现的阻塞队列，可以当做无界队列也可以当做有界队列来使用，同样满足 FIFO 的特性，与 <code>ArrayBlockingQueue</code> 相比起来具有更高的吞吐量，为了防止 <code>LinkedBlockingQueue</code> 容量迅速增，损耗大量内存。通常在创建 <code>LinkedBlockingQueue</code> 对象时，会指定其大小，如果未指定，容量等于 <code>Integer.MAX_VALUE</code> 。</p>
<hr>
<h6 id="PriorityBlockingQueue"><a href="#PriorityBlockingQueue" class="headerlink" title="PriorityBlockingQueue"></a>PriorityBlockingQueue</h6><p><code>PriorityBlockingQueue</code> 是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序，也可以通过自定义类实现 <code>compareTo()</code> 方法来指定元素排序规则，或者初始化时通过构造器参数 <code>Comparator</code> 来指定排序规则。</p>
<p> <code>PriorityBlockingQueue</code> 只能指定初始的队列大小，后面插入元素的时候，<strong>如果空间不够的话会自动扩容</strong></p>
<hr>
<h5 id="ConcurrentSkipListMap"><a href="#ConcurrentSkipListMap" class="headerlink" title="ConcurrentSkipListMap"></a>ConcurrentSkipListMap</h5><p>在高并发的情况下，你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表，你只需要部分锁即可。这样，在高并发环境下，你就可以拥有更好的性能。而就查询的性能而言，跳表的时间复杂度也是 <strong>O(logn)</strong> 所以在并发数据结构中，JDK 使用跳表来实现一个 Map。</p>
<p><strong>跳表是一种利用空间换时间的算法。</strong></p>
<hr>
<h4 id="AQS详解"><a href="#AQS详解" class="headerlink" title="AQS详解"></a>AQS详解</h4><h5 id="AQS介绍"><a href="#AQS介绍" class="headerlink" title="AQS介绍"></a>AQS介绍</h5><p>AQS 的全称为 <code>AbstractQueuedSynchronizer</code> ，翻译过来的意思就是抽象队列同步器。这个类在 <code>java.util.concurrent.locks</code> 包下面。</p>
<hr>
<h5 id="AQS-原理"><a href="#AQS-原理" class="headerlink" title="AQS 原理"></a>AQS 原理</h5><h6 id="AQS-核心思想"><a href="#AQS-核心思想" class="headerlink" title="AQS 核心思想"></a>AQS 核心思想</h6><p>AQS 核心思想是，如果被请求的共享资源空闲，则将当前请求资源的线程设置为有效的工作线程，并且将共享资源设置为锁定状态。如果被请求的共享资源被占用，那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制，这个机制 AQS 是用 <strong>CLH 队列锁</strong> 实现的，即将暂时获取不到锁的线程加入到队列中。</p>
<p>CLH(Craig,Landin,and Hagersten) 队列是一个虚拟的双向队列（虚拟的双向队列即不存在队列实例，仅存在结点之间的关联关系）。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点（Node）来实现锁的分配。在 CLH 同步队列中，一个节点表示一个线程，它保存着线程的引用（thread）、 当前节点在队列中的状态（waitStatus）、前驱节点（prev）、后继节点（next）。</p>
<hr>
<h5 id="AQS-资源共享方式"><a href="#AQS-资源共享方式" class="headerlink" title="AQS 资源共享方式"></a>AQS 资源共享方式</h5><p>两种资源共享方式：<code>Exclusive</code>（独占，只有一个线程能执行，如<code>ReentrantLock</code>）和<code>Share</code>（共享，多个线程可同时执行，如<code>Semaphore</code>&#x2F;<code>CountDownLatch</code>）。</p>
<hr>
<h5 id="常见同步工具类"><a href="#常见同步工具类" class="headerlink" title="常见同步工具类"></a>常见同步工具类</h5><h6 id="Semaphore-信号量"><a href="#Semaphore-信号量" class="headerlink" title="Semaphore(信号量)"></a>Semaphore(信号量)</h6><h6 id="CountDownLatch-（倒计时器）"><a href="#CountDownLatch-（倒计时器）" class="headerlink" title="CountDownLatch （倒计时器）"></a>CountDownLatch （倒计时器）</h6><p> <code>CountDownLatch</code> 允许 <code>count</code> 个线程阻塞在一个地方，直至所有线程的任务都执行完毕。</p>
<p><code>CountDownLatch</code> 是一次性的，计数器的值只能在构造方法中初始化一次，之后没有任何机制再次对其设置值，当 <code>CountDownLatch</code> 使用完毕后，它不能再次被使用。</p>
<hr>
<h6 id="CyclicBarrier-循环栅栏"><a href="#CyclicBarrier-循环栅栏" class="headerlink" title="CyclicBarrier(循环栅栏)"></a>CyclicBarrier(循环栅栏)</h6><p><code>CyclicBarrier</code> 的字面意思是可循环使用（Cyclic）的屏障（Barrier）。它要做的事情是：让一组线程到达一个屏障（也可以叫同步点）时被阻塞，直到最后一个线程到达屏障时，屏障才会开门，所有被屏障拦截的线程才会继续干活。</p>
<hr>
<h4 id="Atomic-原子类总结"><a href="#Atomic-原子类总结" class="headerlink" title="Atomic 原子类总结"></a>Atomic 原子类总结</h4><h5 id="Atomic-原子类介绍"><a href="#Atomic-原子类介绍" class="headerlink" title="Atomic 原子类介绍"></a>Atomic 原子类介绍</h5><p>Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候，一个操作一旦开始，就不会被其他线程干扰。</p>
<p><strong>基本类型</strong></p>
<ul>
<li><code>AtomicInteger</code>：整型原子类</li>
<li><code>AtomicLong</code>：长整型原子类</li>
<li><code>AtomicBoolean</code> ：布尔型原子类</li>
</ul>
<p><strong>数组类型</strong></p>
<ul>
<li><code>AtomicIntegerArray</code>：整型数组原子类</li>
<li><code>AtomicLongArray</code>：长整型数组原子类</li>
<li><code>AtomicReferenceArray</code> ：引用类型数组原子类</li>
</ul>
<p><strong>引用类型</strong></p>
<ul>
<li><code>AtomicReference</code>：引用类型原子类</li>
<li><code>AtomicMarkableReference</code>：原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来，也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。</li>
<li><code>AtomicStampedReference</code> ：原子更新带有版本号的引用类型。该类将整数值与引用关联起来，可用于解决原子的更新数据和数据的版本号，可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。</li>
</ul>
<p><strong>对象的属性修改类型</strong></p>
<ul>
<li><code>AtomicIntegerFieldUpdater</code>:原子更新整型字段的更新器</li>
<li><code>AtomicLongFieldUpdater</code>：原子更新长整型字段的更新器</li>
<li><code>AtomicReferenceFieldUpdater</code>：原子更新引用类型里的字段</li>
</ul>
<hr>
<h5 id="基本类型原子类"><a href="#基本类型原子类" class="headerlink" title="基本类型原子类"></a>基本类型原子类</h5><h6 id="基本数据类型原子类的优势"><a href="#基本数据类型原子类的优势" class="headerlink" title="基本数据类型原子类的优势"></a>基本数据类型原子类的优势</h6><h6 id="AtomicInteger-线程安全原理简单分析"><a href="#AtomicInteger-线程安全原理简单分析" class="headerlink" title="AtomicInteger 线程安全原理简单分析"></a>AtomicInteger 线程安全原理简单分析</h6><h5 id="数组类型原子类"><a href="#数组类型原子类" class="headerlink" title="数组类型原子类"></a>数组类型原子类</h5><h5 id="引用类型原子类"><a href="#引用类型原子类" class="headerlink" title="引用类型原子类"></a>引用类型原子类</h5><h5 id="对象的属性修改类型原子类"><a href="#对象的属性修改类型原子类" class="headerlink" title="对象的属性修改类型原子类"></a>对象的属性修改类型原子类</h5><hr>
<h4 id="ThreadLocal-详解"><a href="#ThreadLocal-详解" class="headerlink" title="ThreadLocal 详解"></a>ThreadLocal 详解</h4><p>线程的变量副本，每个线程隔离。</p>
<ul>
<li><code>ThreadLocal</code>的 key 是<strong>弱引用</strong>，那么在 <code>ThreadLocal.get()</code>的时候，发生<strong>GC</strong>之后，key 是否为<strong>null</strong>？</li>
<li><code>ThreadLocal</code>中<code>ThreadLocalMap</code>的<strong>数据结构</strong>？</li>
<li><code>ThreadLocalMap</code>的<strong>Hash 算法</strong>？</li>
<li><code>ThreadLocalMap</code>中<strong>Hash 冲突</strong>如何解决？</li>
<li><code>ThreadLocalMap</code>的<strong>扩容机制</strong>？</li>
<li><code>ThreadLocalMap</code>中<strong>过期 key 的清理机制</strong>？<strong>探测式清理</strong>和<strong>启发式清理</strong>流程？</li>
<li><code>ThreadLocalMap.set()</code>方法实现原理？</li>
<li><code>ThreadLocalMap.get()</code>方法实现原理？</li>
<li>项目中<code>ThreadLocal</code>使用情况？遇到的坑？</li>
</ul>
<hr>
<h6 id="ThreadLocal的数据结构"><a href="#ThreadLocal的数据结构" class="headerlink" title="ThreadLocal的数据结构"></a><code>ThreadLocal</code>的数据结构</h6><p><code>Thread</code>类有一个类型为<code>ThreadLocal.ThreadLocalMap</code>的实例变量<code>threadLocals</code>，也就是说每个线程有一个自己的<code>ThreadLocalMap</code>。</p>
<p><code>ThreadLocalMap</code>有自己的独立实现，可以简单地将它的<code>key</code>视作<code>ThreadLocal</code>，<code>value</code>为代码中放入的值（实际上<code>key</code>并不是<code>ThreadLocal</code>本身，而是它的一个<strong>弱引用</strong>）。</p>
<p>每个线程在往<code>ThreadLocal</code>里放值的时候，都会往自己的<code>ThreadLocalMap</code>里存，读也是以<code>ThreadLocal</code>作为引用，在自己的<code>map</code>里找对应的<code>key</code>，从而实现了<strong>线程隔离</strong>。</p>
<p><code>ThreadLocalMap</code>有点类似<code>HashMap</code>的结构，只是<code>HashMap</code>是由<strong>数组+链表</strong>实现的，而<code>ThreadLocalMap</code>中并没有<strong>链表</strong>结构。</p>
<p>我们还要注意<code>Entry</code>， 它的<code>key</code>是<code>ThreadLocal&lt;?&gt; k</code> ，继承自<code>WeakReference</code>， 也就是我们常说的弱引用类型。</p>
<hr>
<h6 id="GC-之后-key-是否为-null？"><a href="#GC-之后-key-是否为-null？" class="headerlink" title="GC 之后 key 是否为 null？"></a>GC 之后 key 是否为 null？</h6><p><strong>四种引用类型</strong>：</p>
<ul>
<li><strong>强引用</strong>：我们常常 new 出来的对象就是强引用类型，只要强引用存在，垃圾回收器将永远不会回收被引用的对象，哪怕内存不足的时候</li>
<li><strong>软引用</strong>：使用 SoftReference 修饰的对象被称为软引用，软引用指向的对象在内存要溢出的时候被回收</li>
<li><strong>弱引用</strong>：使用 WeakReference 修饰的对象被称为弱引用，只要发生垃圾回收，若这个对象只被弱引用指向，那么就会被回收</li>
<li><strong>虚引用</strong>：虚引用是最弱的引用，在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知</li>
</ul>
<hr>
<h6 id="ThreadLocal-set-方法源码详解"><a href="#ThreadLocal-set-方法源码详解" class="headerlink" title="ThreadLocal.set()方法源码详解"></a><code>ThreadLocal.set()</code>方法源码详解</h6><p><code>ThreadLocal</code>中的<code>set</code>方法原理如上图所示，很简单，主要是判断<code>ThreadLocalMap</code>是否存在，然后使用<code>ThreadLocal</code>中的<code>set</code>方法进行数据处理。</p>
<hr>
<h6 id="ThreadLocalMap-Hash-算法"><a href="#ThreadLocalMap-Hash-算法" class="headerlink" title="ThreadLocalMap Hash 算法"></a><code>ThreadLocalMap</code> Hash 算法</h6><p>既然是<code>Map</code>结构，那么<code>ThreadLocalMap</code>当然也要实现自己的<code>hash</code>算法来解决散列表数组冲突问题。</p>
<p>每当创建一个<code>ThreadLocal</code>对象，这个<code>ThreadLocal.nextHashCode</code> 这个值就会增长 <code>0x61c88647</code> 。</p>
<p>这个值很特殊，它是<strong>斐波那契数</strong> 也叫 <strong>黄金分割数</strong>。<code>hash</code>增量为 这个数字，带来的好处就是 <code>hash</code> <strong>分布非常均匀</strong>。</p>
<hr>
<h6 id="ThreadLocalMap-Hash-冲突"><a href="#ThreadLocalMap-Hash-冲突" class="headerlink" title="ThreadLocalMap Hash 冲突"></a><code>ThreadLocalMap</code> Hash 冲突</h6><hr>
<h6 id="ThreadLocalMap-set-详解"><a href="#ThreadLocalMap-set-详解" class="headerlink" title="ThreadLocalMap.set()详解"></a><code>ThreadLocalMap.set()</code>详解</h6><h6 id=""><a href="#" class="headerlink" title=""></a></h6><p><strong>第一种情况：</strong> 通过<code>hash</code>计算后的槽位对应的<code>Entry</code>数据为空：</p>
<p>这里直接将数据放到该槽位即可。</p>
<p><strong>第二种情况：</strong> 槽位数据不为空，<code>key</code>值与当前<code>ThreadLocal</code>通过<code>hash</code>计算获取的<code>key</code>值一致：</p>
<p>这里直接更新该槽位的数据。</p>
<p><strong>第三种情况：</strong> 槽位数据不为空，往后遍历过程中，在找到<code>Entry</code>为<code>null</code>的槽位之前，没有遇到<code>key</code>过期的<code>Entry</code>：</p>
<p>遍历散列数组，线性往后查找，如果找到<code>Entry</code>为<code>null</code>的槽位，则将数据放入该槽位中，或者往后遍历过程中，遇到了<strong>key 值相等</strong>的数据，直接更新即可。</p>
<p><strong>第四种情况：</strong> 槽位数据不为空，往后遍历过程中，在找到<code>Entry</code>为<code>null</code>的槽位之前，遇到<code>key</code>过期的<code>Entry</code>，此时就会执行<code>replaceStaleEntry()</code>方法，该方法含义是<strong>替换过期数据的逻辑</strong>，进行探测式数据清理工作。</p>
<p>以当前<code>staleSlot</code>开始 向前迭代查找，找其他过期的数据，然后更新过期数据起始扫描下标<code>slotToExpunge</code>。<code>for</code>循环迭代，直到碰到<code>Entry</code>为<code>null</code>结束。</p>
<p>如果找到了过期的数据，继续向前迭代，直到遇到<code>Entry=null</code>的槽位才停止迭代。</p>
<hr>
<h6 id="ThreadLocalMap-set-源码详解"><a href="#ThreadLocalMap-set-源码详解" class="headerlink" title="ThreadLocalMap.set()源码详解"></a><code>ThreadLocalMap.set()</code>源码详解</h6><hr>
<h6 id="ThreadLocalMap过期-key-的探测式清理流程"><a href="#ThreadLocalMap过期-key-的探测式清理流程" class="headerlink" title="ThreadLocalMap过期 key 的探测式清理流程"></a><code>ThreadLocalMap</code>过期 key 的探测式清理流程</h6><p><code>ThreadLocalMap</code>的两种过期<code>key</code>数据清理方式：<strong>探测式清理</strong>和<strong>启发式清理</strong>。</p>
<p>探测式清理，也就是<code>expungeStaleEntry</code>方法，遍历散列数组，从开始位置向后探测清理过期数据，将过期数据的<code>Entry</code>设置为<code>null</code>，沿途中碰到未过期的数据则将此数据<code>rehash</code>后重新在<code>table</code>数组中定位，如果定位的位置已经有了数据，则会将未过期的数据放到最靠近此位置的<code>Entry=null</code>的桶中，使<code>rehash</code>后的<code>Entry</code>数据距离正确的桶的位置更近一些。操作逻辑如下：</p>
<hr>
<h6 id="ThreadLocalMap扩容机制"><a href="#ThreadLocalMap扩容机制" class="headerlink" title="ThreadLocalMap扩容机制"></a><code>ThreadLocalMap</code>扩容机制</h6><p>在<code>ThreadLocalMap.set()</code>方法的最后，如果执行完启发式清理工作后，未清理到任何数据，且当前散列数组中<code>Entry</code>的数量已经达到了列表的扩容阈值<code>(len*2/3)</code>，就开始执行&#x3D;&#x3D;<code>rehash()</code>&#x3D;&#x3D;逻辑。</p>
<p><code>table</code>中可能有一些<code>key</code>为<code>null</code>的<code>Entry</code>数据被清理掉，所以此时通过判断<code>size &gt;= threshold - threshold / 4</code> 也就是<code>size &gt;= threshold * 3/4</code> 来决定是否扩容（&#x3D;&#x3D;<code>resize</code>&#x3D;&#x3D;）。</p>
<p><img src="https://javaguide.cn/assets/24.ec7f7610.png" alt="img"></p>
<hr>
<h6 id="ThreadLocalMap-get-详解"><a href="#ThreadLocalMap-get-详解" class="headerlink" title="ThreadLocalMap.get()详解"></a><code>ThreadLocalMap.get()</code>详解</h6><p><strong>第一种情况：</strong> 通过查找<code>key</code>值计算出散列表中<code>slot</code>位置，然后该<code>slot</code>位置中的<code>Entry.key</code>和查找的<code>key</code>一致，则直接返回</p>
<p><strong>第二种情况：</strong> <code>slot</code>位置中的<code>Entry.key</code>和要查找的<code>key</code>不一致</p>
<p>我们以<code>get(ThreadLocal1)</code>为例，通过<code>hash</code>计算后，正确的<code>slot</code>位置应该是 4，而<code>index=4</code>的槽位已经有了数据，且<code>key</code>值不等于<code>ThreadLocal1</code>，所以需要继续往后迭代查找。</p>
<p>迭代到<code>index=5</code>的数据时，此时<code>Entry.key=null</code>，触发一次探测式数据回收操作，执行<code>expungeStaleEntry()</code>方法，执行完后，<code>index 5,8</code>的数据都会被回收，而<code>index 6,7</code>的数据都会前移。<code>index 6,7</code>前移之后，继续从 <code>index=5</code> 往后迭代，于是就在 <code>index=5</code> 找到了<code>key</code>值相等的<code>Entry</code>数据</p>
<p><img src="https://javaguide.cn/assets/27.9c78c2a2.png" alt="img"></p>
<p><img src="https://javaguide.cn/assets/28.ea7d5196.png" alt="img"></p>
<h6 id="ThreadLocalMap过期-key-的启发式清理流程"><a href="#ThreadLocalMap过期-key-的启发式清理流程" class="headerlink" title="ThreadLocalMap过期 key 的启发式清理流程"></a><code>ThreadLocalMap</code>过期 key 的启发式清理流程</h6><p>探测式清理是以当前<code>Entry</code> 往后清理，遇到值为<code>null</code>则结束清理，属于<strong>线性探测清理</strong>。</p>
<hr>
<h6 id="InheritableThreadLocal"><a href="#InheritableThreadLocal" class="headerlink" title="InheritableThreadLocal"></a><code>InheritableThreadLocal</code></h6><hr>
<h5 id="CompletableFuture入门"><a href="#CompletableFuture入门" class="headerlink" title="CompletableFuture入门"></a>CompletableFuture入门</h5><p><code>Future</code> 接口有 5 个方法：</p>
<ul>
<li><code>boolean cancel(boolean mayInterruptIfRunning)</code> ：尝试取消执行任务。</li>
<li><code>boolean isCancelled()</code> ：判断任务是否被取消。</li>
<li><code>boolean isDone()</code> ： 判断任务是否已经被执行完成。</li>
<li><code>get()</code> ：等待任务执行完成并获取运算结果。</li>
<li><code>get(long timeout, TimeUnit unit)</code> ：多了一个超时时间。</li>
</ul>
<hr>
<h6 id="常见操作"><a href="#常见操作" class="headerlink" title="常见操作"></a>常见操作</h6><p> <strong>组合 CompletableFuture</strong></p>
<p><strong>那 <code>thenCompose()</code> 和 <code>thenCombine()</code> 有什么区别呢？</strong></p>
<ul>
<li><code>thenCompose()</code> 可以两个 <code>CompletableFuture</code> 对象，并将前一个任务的返回结果作为下一个任务的参数，它们之间存在着先后顺序。</li>
<li><code>thenCombine()</code> 会在两个任务都执行完成后，把两个任务的结果合并。两个任务是并行执行的，它们之间并没有先后依赖顺序。</li>
</ul>
<hr>
<p>并行运行多个 CompletableFuture</p>
<p>经常和 <code>allOf()</code> 方法拿来对比的是 <code>anyOf()</code> 方法。</p>
<p><strong><code>allOf()</code> 方法会等到所有的 <code>CompletableFuture</code> 都运行完成之后再返回</strong></p>
<p>调用 <code>join()</code> 可以让程序等<code>future1</code> 和 <code>future2</code> 都运行完了之后再继续执行。</p>
<p><strong><code>anyOf()</code> 方法不会等待所有的 <code>CompletableFuture</code> 都运行完成之后再返回，只要有一个执行完成即可！</strong></p>
<hr>
<h2 id="JVM"><a href="#JVM" class="headerlink" title="JVM"></a>JVM</h2><h3 id="Java-内存区域详解"><a href="#Java-内存区域详解" class="headerlink" title="Java 内存区域详解"></a>Java 内存区域详解</h3><p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/jvm/java-runtime-data-areas-jdk1.8.png" alt="Java 运行时数据区域（JDK1.8 之后）"></p>
<h4 id="运行时数据区域"><a href="#运行时数据区域" class="headerlink" title="运行时数据区域"></a>运行时数据区域</h4><p><strong>线程私有的：</strong></p>
<ul>
<li>程序计数器</li>
<li>虚拟机栈</li>
<li>本地方法栈</li>
</ul>
<p><strong>线程共享的：</strong></p>
<ul>
<li>堆</li>
<li>方法区</li>
<li>直接内存 (非运行时数据区的一部分)</li>
</ul>
<h5 id="程序计数器"><a href="#程序计数器" class="headerlink" title="程序计数器"></a>程序计数器</h5><p>程序计数器是一块较小的内存空间，可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令，分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。</p>
<p>另外，为了线程切换后能恢复到正确的执行位置，每条线程都需要有一个独立的程序计数器，各线程之间计数器互不影响，独立存储，我们称这类内存区域为“线程私有”的内存。</p>
<p>从上面的介绍中我们知道了程序计数器主要有两个作用：</p>
<ul>
<li>字节码解释器通过改变程序计数器来依次读取指令，从而实现代码的流程控制，如：顺序执行、选择、循环、异常处理。</li>
<li>在多线程的情况下，程序计数器用于记录当前线程执行的位置，从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。</li>
</ul>
<p>⚠️ 注意 ：程序计数器是唯一一个不会出现 <code>OutOfMemoryError</code> 的内存区域，它的生命周期随着线程的创建而创建，随着线程的结束而死亡。</p>
<hr>
<h5 id="Java-虚拟机栈"><a href="#Java-虚拟机栈" class="headerlink" title="Java 虚拟机栈"></a>Java 虚拟机栈</h5><p>栈由一个个栈帧组成，而每个栈帧中都拥有：局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似，两者都是先进后出的数据结构，只支持出栈和入栈两种操作。</p>
<p><strong>局部变量表</strong> 主要存放了编译期可知的各种数据类型（boolean、byte、char、short、int、float、long、double）、对象引用（reference 类型，它不同于对象本身，可能是一个指向对象起始地址的引用指针，也可能是指向一个代表对象的句柄或其他与此对象相关的位置）。</p>
<p><strong>操作数栈</strong> 主要作为方法调用的中转站使用，用于存放方法执行过程中产生的中间计算结果。另外，计算过程中产生的临时变量也会放在操作数栈中。</p>
<p><strong>动态链接</strong> 主要服务一个方法需要调用其他方法的场景。在 Java 源文件被编译成字节码文件时，所有的变量和方法引用都作为符号引用（Symbilic Reference）保存在 Class 文件的常量池里。当一个方法要调用其他方法，需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了&#x3D;&#x3D;将符号引用转换为调用方法的直接引用&#x3D;&#x3D;。</p>
<p>Java 方法有两种返回方式，一种是 return 语句正常返回，一种是抛出异常。不管哪种返回方式，都会 栈帧被弹出。也就是说， **栈帧随着方法调用而创建，随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。</p>
<p>简单总结一下程序运行中栈可能会出现两种错误：</p>
<ul>
<li><strong><code>StackOverFlowError</code>：</strong> 若栈的内存大小不允许动态扩展，那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候，就抛出 <code>StackOverFlowError</code> 错误。</li>
<li><strong><code>OutOfMemoryError</code>：</strong> 如果栈的内存大小可以动态扩展， 如果虚拟机在动态扩展栈时无法申请到足够的内存空间，则抛出<code>OutOfMemoryError</code>异常。</li>
</ul>
<hr>
<h5 id="本地方法栈"><a href="#本地方法栈" class="headerlink" title="本地方法栈"></a>本地方法栈</h5><p>和虚拟机栈所发挥的作用非常相似，区别是： <strong>虚拟机栈为虚拟机执行 Java 方法 （也就是字节码）服务，而本地方法栈则为虚拟机使用到的 Native 方法服务。</strong> 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。</p>
<hr>
<h5 id="堆"><a href="#堆" class="headerlink" title="堆"></a>堆</h5><p><strong>JDK 8 版本之后 PermGen(永久) 已被 Metaspace(元空间) 取代，元空间使用的是直接内存</strong> （我会在方法区这部分内容详细介绍到）。</p>
<hr>
<h5 id="方法区"><a href="#方法区" class="headerlink" title="方法区"></a>方法区</h5><p>方法区会存储已被虚拟机加载的 <strong>类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据</strong>。</p>
<hr>
<h5 id="运行时常量池"><a href="#运行时常量池" class="headerlink" title="运行时常量池"></a>运行时常量池</h5><p>Class 文件中除了有类的版本、字段、方法、接口等描述信息外，还有用于存放编译期生成的各种字面量（Literal）和符号引用（Symbolic Reference）的 <strong>常量池表(Constant Pool Table)</strong> 。</p>
<hr>
<h5 id="字符串常量池"><a href="#字符串常量池" class="headerlink" title="字符串常量池"></a>字符串常量池</h5><p><strong>字符串常量池</strong> 是 JVM 为了提升性能和减少内存消耗针对字符串（String 类）专门开辟的一块区域，主要目的是为了避免字符串的重复创建。</p>
<p><strong><code>StringTable</code> 中保存的是字符串对象的引用，字符串对象的引用指向堆中的字符串对象。</strong></p>
<p>JDK1.7 之前，字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。</p>
<p><img src="G:\GitDocument\Study\images\面试.assets\image-20230207142812717.png" alt="image-20230207142812717"></p>
<blockquote>
<p><strong>运行时常量池、方法区、字符串常量池这些都是不随虚拟机实现而改变的逻辑概念，是公共且抽象的，Metaspace、Heap 是与具体某种虚拟机实现相关的物理概念，是私有且具体的。</strong></p>
</blockquote>
<h5 id="x3D-x3D-直接内存-x3D-x3D"><a href="#x3D-x3D-直接内存-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;直接内存&#x3D;&#x3D;"></a>&#x3D;&#x3D;直接内存&#x3D;&#x3D;</h5><p><a target="_blank" rel="noopener" href="https://blog.csdn.net/linzherong/article/details/123820944">https://blog.csdn.net/linzherong/article/details/123820944</a></p>
<p><strong>BIO模式下</strong></p>
<p><img src="https://img-blog.csdnimg.cn/75930bcb2b174aa5b63027ae9982242a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LuO6I-c6bif5Yiw5pS-5byD,size_20,color_FFFFFF,t_70,g_se,x_16" alt="img"></p>
<p><strong>NIO模式下</strong></p>
<p><img src="https://img-blog.csdnimg.cn/b58dd3ca6ce04c119db8b64b8bc56623.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LuO6I-c6bif5Yiw5pS-5byD,size_20,color_FFFFFF,t_70,g_se,x_16" alt="img"></p>
<p>JDK1.4 中新加入的 <strong>NIO(New Input&#x2F;Output) 类</strong>，引入了一种基于**通道（Channel）**与**缓存区（Buffer）**的 I&#x2F;O 方式，它可以直接使用 Native 函数库直接分配堆外内存，然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能，因为**避免了在 Java 堆和 Native 堆之间来回复制数据**。</p>
<p><img src="https://img-blog.csdnimg.cn/20200603000502908.png" alt="img"></p>
<p><strong>&#x3D;&#x3D;零拷贝&#x3D;&#x3D;</strong></p>
<hr>
<h4 id="HotSpot-虚拟机对象探秘"><a href="#HotSpot-虚拟机对象探秘" class="headerlink" title="HotSpot 虚拟机对象探秘"></a>HotSpot 虚拟机对象探秘</h4><h5 id="对象的创建"><a href="#对象的创建" class="headerlink" title="对象的创建"></a>对象的创建</h5><h6 id="Step1-类加载检查"><a href="#Step1-类加载检查" class="headerlink" title="Step1:类加载检查"></a>Step1:类加载检查</h6><h6 id="Step2-分配内存"><a href="#Step2-分配内存" class="headerlink" title="Step2:分配内存"></a>Step2:分配内存</h6><p>在<strong>类加载检查</strong>通过后，接下来虚拟机将为新生对象<strong>分配内存</strong>。对象所需的内存大小在类加载完成后便可确定，为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。<strong>分配方式</strong>有 <strong>“指针碰撞”</strong> 和 <strong>“空闲列表”</strong> 两种，<strong>选择哪种分配方式由 Java 堆是否规整决定，而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定</strong>。</p>
<p><strong>内存分配的两种方式</strong> （补充内容，需要掌握）：</p>
<ul>
<li>指针碰撞 ： <ul>
<li>适用场合 ：堆内存规整（即没有内存碎片）的情况下。</li>
<li>原理 ：用过的内存全部整合到一边，没有用过的内存放在另一边，中间有一个分界指针，只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。</li>
<li>使用该分配方式的 GC 收集器：Serial, ParNew</li>
</ul>
</li>
<li>空闲列表 ： <ul>
<li>适用场合 ： 堆内存不规整的情况下。</li>
<li>原理 ：虚拟机会维护一个列表，该列表中会记录哪些内存块是可用的，在分配的时候，找一块儿足够大的内存块儿来划分给对象实例，最后更新列表记录。</li>
<li>使用该分配方式的 GC 收集器：CMS</li>
</ul>
</li>
</ul>
<p><strong>内存分配并发问题（补充内容，需要掌握）</strong></p>
<p><strong>CAS+失败重试：</strong> CAS 是乐观锁的一种实现方式。所谓乐观锁就是，每次不加锁而是假设没有冲突而去完成某项操作，如果因为冲突失败就重试，直到成功为止。<strong>虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。</strong></p>
<p><strong>TLAB：</strong> 为每一个线程预先在 Eden 区分配一块儿内存，JVM 在给线程中的对象分配内存时，首先在 TLAB 分配，当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时，再采用上述的 CAS 进行内存分配</p>
<h6 id="Step3-初始化零值"><a href="#Step3-初始化零值" class="headerlink" title="Step3:初始化零值"></a>Step3:初始化零值</h6><p>内存分配完成后，虚拟机需要将分配到的内存空间都初始化为零值（不包括对象头），这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用，程序能访问到这些字段的数据类型所对应的零值。</p>
<h6 id="Step4-设置对象头"><a href="#Step4-设置对象头" class="headerlink" title="Step4:设置对象头"></a>Step4:设置对象头</h6><p>初始化零值完成之后，<strong>虚拟机要对对象进行必要的设置</strong>，例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 <strong>这些信息存放在对象头中。</strong> 另外，根据虚拟机当前运行状态的不同，如是否启用偏向锁等，对象头会有不同的设置方式。</p>
<h6 id="Step5-执行-init-方法"><a href="#Step5-执行-init-方法" class="headerlink" title="Step5:执行 init 方法"></a>Step5:执行 init 方法</h6><p>在上面工作都完成之后，从虚拟机的视角来看，一个新的对象已经产生了，但从 Java 程序的视角来看，对象创建才刚开始，<code>&lt;init&gt;</code> 方法还没有执行，所有的字段都还为零。所以一般来说，执行 new 指令之后会接着执行 <code>&lt;init&gt;</code> 方法，把对象按照程序员的意愿进行初始化，这样一个真正可用的对象才算完全产生出来。</p>
<hr>
<h4 id="对象的内存布局"><a href="#对象的内存布局" class="headerlink" title="对象的内存布局"></a>对象的内存布局</h4><p><strong>对象头</strong>、<strong>实例数据</strong>和<strong>对齐填充</strong></p>
<p><strong>Hotspot 虚拟机的对象头包括两部分信息</strong>，<strong>第一部分用于存储对象自身的运行时数据</strong>（哈希码、GC 分代年龄、锁状态标志等等），<strong>另一部分是类型指针</strong>，即对象指向它的类元数据的指针，虚拟机通过这个指针来确定这个对象是哪个类的实例。</p>
<p><strong>实例数据部分是对象真正存储的有效信息</strong>，也是在程序中所定义的各种类型的字段内容。</p>
<p><strong>对齐填充部分不是必然存在的，也没有什么特别的含义，仅仅起占位作用。</strong> 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍，换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数（1 倍或 2 倍），因此，当对象实例数据部分没有对齐时，就需要通过对齐填充来补全。</p>
<hr>
<h4 id="对象的访问定位"><a href="#对象的访问定位" class="headerlink" title="对象的访问定位"></a>对象的访问定位</h4><h5 id="句柄"><a href="#句柄" class="headerlink" title="句柄"></a>句柄</h5><p>如果使用句柄的话，那么 Java 堆中将会划分出一块内存来作为句柄池，reference 中存储的就是对象的句柄地址，而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。<img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/jvm/access-location-of-object-handle.png" alt="对象的访问定位-使用句柄"></p>
<h5 id="直接指针"><a href="#直接指针" class="headerlink" title="直接指针"></a>直接指针</h5><p>如果使用直接指针访问，reference 中存储的直接就是对象的地址。<img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/jvm/access-location-of-object-handle-direct-pointer.png" alt="对象的访问定位-直接指针"></p>
<hr>
<h3 id="JVM-垃圾回收详解"><a href="#JVM-垃圾回收详解" class="headerlink" title="JVM 垃圾回收详解"></a>JVM 垃圾回收详解</h3><h4 id="堆空间的基本结构"><a href="#堆空间的基本结构" class="headerlink" title="堆空间的基本结构"></a>堆空间的基本结构</h4><p>Java 堆是垃圾收集器管理的主要区域，因此也被称作 <strong>GC 堆</strong></p>
<h4 id="内存分配和回收原则"><a href="#内存分配和回收原则" class="headerlink" title="内存分配和回收原则"></a>内存分配和回收原则</h4><h5 id="对象优先在-Eden-区分配"><a href="#对象优先在-Eden-区分配" class="headerlink" title="对象优先在 Eden 区分配"></a>对象优先在 Eden 区分配</h5><h5 id="大对象直接进入老年代"><a href="#大对象直接进入老年代" class="headerlink" title="大对象直接进入老年代"></a>大对象直接进入老年代</h5><p>大对象就是需要大量连续内存空间的对象（比如：字符串、数组）。</p>
<p>大对象直接进入老年代主要是为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。</p>
<hr>
<h5 id="长期存活的对象将进入老年代"><a href="#长期存活的对象将进入老年代" class="headerlink" title="长期存活的对象将进入老年代"></a>长期存活的对象将进入老年代</h5><p><strong>关于默认的晋升年龄是 15，这个说法的来源大部分都是《深入理解 Java 虚拟机》这本书。</strong> <strong>默认晋升年龄并不都是 15，这个是要区分垃圾收集器的，CMS 就是 6</strong>.</p>
<hr>
<h5 id="主要进行-gc-的区域"><a href="#主要进行-gc-的区域" class="headerlink" title="主要进行 gc 的区域"></a>主要进行 gc 的区域</h5><p>针对 HotSpot VM 的实现，它里面的 GC 其实准确分类只有两大种：</p>
<p>部分收集 (Partial GC)：</p>
<ul>
<li>新生代收集（Minor GC &#x2F; Young GC）：只对新生代进行垃圾收集；</li>
<li>老年代收集（Major GC &#x2F; Old GC）：只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集；</li>
<li>混合收集（Mixed GC）：对整个新生代和部分老年代进行垃圾收集。</li>
</ul>
<p>整堆收集 (Full GC)：收集整个 Java 堆和方法区。</p>
<hr>
<h5 id="空间分配担保"><a href="#空间分配担保" class="headerlink" title="空间分配担保"></a>空间分配担保</h5><p>空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。</p>
<hr>
<h4 id="死亡对象判断方法"><a href="#死亡对象判断方法" class="headerlink" title="死亡对象判断方法"></a>死亡对象判断方法</h4><h5 id="引用计数法"><a href="#引用计数法" class="headerlink" title="引用计数法"></a>引用计数法</h5><ul>
<li>每当有一个地方引用它，计数器就加 1；</li>
<li>当引用失效，计数器就减 1；</li>
<li>任何时候计数器为 0 的对象就是不可能再被使用的。</li>
</ul>
<p><strong>这个方法实现简单，效率高，但是目前主流的虚拟机中并没有选择这个算法来管理内存，其最主要的原因是它很难解决对象之间相互循环引用的问题。</strong></p>
<h5 id="可达性分析算法"><a href="#可达性分析算法" class="headerlink" title="可达性分析算法"></a>可达性分析算法</h5><p>这个算法的基本思想就是通过一系列的称为 <strong>“GC Roots”</strong> 的对象作为起点，从这些节点开始向下搜索，节点所走过的路径称为引用链，当一个对象到 GC Roots 没有任何引用链相连的话，则证明此对象是不可用的，需要被回收。</p>
<p><strong>哪些对象可以作为 GC Roots 呢？</strong></p>
<ul>
<li>虚拟机栈(栈帧中的局部变量表)中引用的对象</li>
<li>本地方法栈(Native 方法)中引用的对象</li>
<li>方法区中类静态属性引用的对象</li>
<li>方法区中常量引用的对象</li>
<li>所有被同步锁持有的对象</li>
</ul>
<p><strong>对象可以被回收，就代表一定会被回收吗？</strong></p>
<p>即使在可达性分析法中不可达的对象，也并非是“非死不可”的，这时候它们暂时处于“缓刑阶段”，要真正宣告一个对象死亡，至少要经历两次标记过程；可达性分析法中不可达的对象被第一次标记并且进行一次筛选，筛选的条件是此对象是否有必要执行 <code>finalize</code> 方法。当对象没有覆盖 <code>finalize</code> 方法，或 <code>finalize</code> 方法已经被虚拟机调用过时，虚拟机将这两种情况视为没有必要执行。</p>
<hr>
<h5 id="引用类型总结"><a href="#引用类型总结" class="headerlink" title="引用类型总结"></a>引用类型总结</h5><h6 id="1-强引用（StrongReference）"><a href="#1-强引用（StrongReference）" class="headerlink" title="1.强引用（StrongReference）"></a><strong>1.强引用（StrongReference）</strong></h6><p>以前我们使用的大部分引用实际上都是强引用，这是使用最普遍的引用。如果一个对象具有强引用，那就类似于<strong>必不可少的生活用品</strong>，垃圾回收器绝不会回收它。当内存空间不足，Java 虚拟机宁愿抛出 OutOfMemoryError 错误，使程序异常终止，也不会靠随意回收具有强引用的对象来解决内存不足问题。</p>
<h6 id="2．软引用（SoftReference）"><a href="#2．软引用（SoftReference）" class="headerlink" title="2．软引用（SoftReference）"></a><strong>2．软引用（SoftReference）</strong></h6><p>如果一个对象只具有软引用，那就类似于<strong>可有可无的生活用品</strong>。如果内存空间足够，垃圾回收器就不会回收它，如果内存空间不足了，就会回收这些对象的内存。只要垃圾回收器没有回收它，该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。</p>
<h6 id="3．弱引用（WeakReference）"><a href="#3．弱引用（WeakReference）" class="headerlink" title="3．弱引用（WeakReference）"></a><strong>3．弱引用（WeakReference）</strong></h6><p>如果一个对象只具有弱引用，那就类似于<strong>可有可无的生活用品</strong>。弱引用与软引用的区别在于：只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中，一旦发现了只具有弱引用的对象，不管当前内存空间足够与否，都会回收它的内存。不过，由于垃圾回收器是一个优先级很低的线程， 因此不一定会很快发现那些只具有弱引用的对象。</p>
<h6 id="4．虚引用（PhantomReference）"><a href="#4．虚引用（PhantomReference）" class="headerlink" title="4．虚引用（PhantomReference）"></a><strong>4．虚引用（PhantomReference）</strong></h6><p>“虚引用”顾名思义，就是形同虚设，与其他几种引用都不同，虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用，那么它就和没有任何引用一样，在任何时候都可能被垃圾回收。</p>
<p>在程序设计中一般很少使用弱引用与虚引用，使用软引用的情况较多，<strong>软引用可以加速 JVM 对垃圾内存的回收速度，可以维护系统的运行安全，防止内存溢出（OutOfMemory）等问题的产生</strong>。</p>
<hr>
<h5 id="如何判断一个常量是废弃常量？"><a href="#如何判断一个常量是废弃常量？" class="headerlink" title="如何判断一个常量是废弃常量？"></a>如何判断一个常量是废弃常量？</h5><ol>
<li><p><strong>JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时 hotspot 虚拟机对方法区的实现为永久代</strong></p>
</li>
<li><p><strong>JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代</strong> 。</p>
</li>
<li><p><strong>JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)</strong></p>
<hr>
</li>
</ol>
<h5 id="如何判断一个类是无用的类"><a href="#如何判断一个类是无用的类" class="headerlink" title="如何判断一个类是无用的类"></a>如何判断一个类是无用的类</h5><p> <strong>“无用的类”</strong> ：</p>
<ul>
<li>该类所有的实例都已经被回收，也就是 Java 堆中不存在该类的任何实例。</li>
<li>加载该类的 <code>ClassLoader</code> 已经被回收。</li>
<li>该类对应的 <code>java.lang.Class</code> 对象没有在任何地方被引用，无法在任何地方通过反射访问该类的方法。</li>
</ul>
<hr>
<h4 id="垃圾收集算法"><a href="#垃圾收集算法" class="headerlink" title="垃圾收集算法"></a>垃圾收集算法</h4><h5 id="标记-清除算法"><a href="#标记-清除算法" class="headerlink" title="标记-清除算法"></a>标记-清除算法</h5><p>该算法分为“标记”和“清除”阶段：首先标记出所有不需要回收的对象，在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法，后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题：</p>
<ol>
<li><strong>效率问题</strong></li>
<li><strong>空间问题（标记清除后会产生大量不连续的碎片）</strong></li>
</ol>
<hr>
<h5 id="标记-复制算法"><a href="#标记-复制算法" class="headerlink" title="标记-复制算法"></a>标记-复制算法</h5><p>为了解决效率问题，“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块，每次使用其中的一块。当这一块的内存使用完后，就将还存活的对象复制到另一块去，然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。</p>
<p><img src="https://javaguide.cn/assets/90984624.e8c186ae.png" alt="复制算法"></p>
<hr>
<h5 id="标记-整理算法"><a href="#标记-整理算法" class="headerlink" title="标记-整理算法"></a>标记-整理算法</h5><p>根据老年代的特点提出的一种标记算法，标记过程仍然与“标记-清除”算法一样，但后续步骤不是直接对可回收对象回收，而是让所有存活的对象向一端移动，然后直接清理掉端边界以外的内存。</p>
<hr>
<h5 id="分代收集算法"><a href="#分代收集算法" class="headerlink" title="分代收集算法"></a>分代收集算法</h5><p>当前虚拟机的垃圾收集都采用分代收集算法，这种算法没有什么新的思想，只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代，这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。</p>
<p><strong>比如在新生代中，每次收集都会有大量对象死去，所以可以选择”标记-复制“算法，只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的，而且没有额外的空间对它进行分配担保，所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。</strong></p>
<hr>
<h4 id="垃圾收集器"><a href="#垃圾收集器" class="headerlink" title="垃圾收集器"></a>垃圾收集器</h4><p><strong>如果说收集算法是内存回收的方法论，那么垃圾收集器就是内存回收的具体实现。</strong></p>
<p><strong>根据具体应用场景选择适合自己的垃圾收集器</strong></p>
<hr>
<h5 id="Serial-收集器"><a href="#Serial-收集器" class="headerlink" title="Serial 收集器"></a>Serial 收集器</h5><p>是一个单线程收集器了。它的 <strong>“单线程”</strong> 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作，更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程（ <strong>“Stop The World”</strong> ），直到它收集结束。</p>
<p><strong>新生代采用标记-复制算法，老年代采用标记-整理算法。</strong></p>
<p><strong>优点</strong></p>
<p><strong>简单而高效（与其他收集器的单线程相比）</strong>。Serial 收集器由于没有线程交互的开销，自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。</p>
<hr>
<h5 id="ParNew-收集器"><a href="#ParNew-收集器" class="headerlink" title="ParNew 收集器"></a>ParNew 收集器</h5><p><strong>ParNew 收集器其实就是 Serial 收集器的多线程版本，除了使用多线程进行垃圾收集外，其余行为（控制参数、收集算法、回收策略等等）和 Serial 收集器完全一样。</strong></p>
<p><strong>新生代采用标记-复制算法，老年代采用标记-整理算法。</strong></p>
<p><strong>并行和并发概念补充：</strong></p>
<ul>
<li><strong>并行（Parallel）</strong> ：指多条垃圾收集线程并行工作，但此时用户线程仍然处于等待状态。</li>
<li><strong>并发（Concurrent）</strong>：指用户线程与垃圾收集线程同时执行（但不一定是并行，可能会交替执行），用户程序在继续运行，而垃圾收集器运行在另一个 CPU 上。</li>
</ul>
<hr>
<h5 id="Parallel-Scavenge-收集器"><a href="#Parallel-Scavenge-收集器" class="headerlink" title="Parallel Scavenge 收集器"></a>Parallel Scavenge 收集器</h5><p>Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器，它看上去几乎和 ParNew 都一样。 <strong>那么它有什么特别之处呢？</strong></p>
<p>Parallel Scavenge 收集器关注点是吞吐量（高效率的利用 CPU）。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间（提高用户体验）。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值</p>
<p><strong>新生代采用标记-复制算法，老年代采用标记-整理算法。</strong></p>
<hr>
<h5 id="Serial-Old-收集器"><a href="#Serial-Old-收集器" class="headerlink" title="Serial Old 收集器"></a>Serial Old 收集器</h5><p><strong>Serial 收集器的老年代版本</strong>，它同样是一个单线程收集器。它主要有两大用途：一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用，另一种用途是作为 CMS 收集器的后备方案。</p>
<hr>
<h5 id="Parallel-Old-收集器"><a href="#Parallel-Old-收集器" class="headerlink" title="Parallel Old 收集器"></a>Parallel Old 收集器</h5><p><strong>Parallel Scavenge 收集器的老年代版本</strong>。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合，都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。</p>
<hr>
<h5 id="CMS-收集器"><a href="#CMS-收集器" class="headerlink" title="CMS 收集器"></a>CMS 收集器</h5><p><strong>CMS（Concurrent Mark Sweep）收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。</strong></p>
<p><strong>CMS（Concurrent Mark Sweep）收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器，它第一次实现了让垃圾收集线程与用户线程（基本上）同时工作。</strong></p>
<p>从名字中的<strong>Mark Sweep</strong>这两个词可以看出，CMS 收集器是一种 <strong>“标记-清除”算法</strong>实现的，它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤：</p>
<ul>
<li><strong>初始标记：</strong> 暂停所有的其他线程，并记录下直接与 root 相连的对象，速度很快 ；</li>
<li><strong>并发标记：</strong> 同时开启 GC 和用户线程，用一个闭包结构去记录可达对象。但在这个阶段结束，这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域，所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。</li>
<li><strong>重新标记：</strong> 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录，这个阶段的停顿时间一般会比初始标记阶段的时间稍长，远远比并发标记阶段时间短</li>
<li><strong>并发清除：</strong> 开启用户线程，同时 GC 线程开始对未标记的区域做清扫。</li>
</ul>
<p>从它的名字就可以看出它是一款优秀的垃圾收集器，主要优点：<strong>并发收集、低停顿</strong>。但是它有下面三个明显的缺点：</p>
<ul>
<li><strong>对 CPU 资源敏感；</strong></li>
<li><strong>无法处理浮动垃圾；</strong></li>
<li><strong>它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。</strong></li>
</ul>
<hr>
<h5 id="G1-收集器"><a href="#G1-收集器" class="headerlink" title="G1 收集器"></a>G1 收集器</h5><p><strong>G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.</strong></p>
<ul>
<li><strong>并行与并发</strong>：G1 能充分利用 CPU、多核环境下的硬件优势，使用多个 CPU（CPU 或者 CPU 核心）来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作，G1 收集器仍然可以通过并发的方式让 java 程序继续执行。</li>
<li><strong>分代收集</strong>：虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆，但是还是保留了分代的概念。</li>
<li><strong>空间整合</strong>：与 CMS 的“标记-清理”算法不同，G1 从整体来看是基于“标记-整理”算法实现的收集器；从局部上来看是基于“标记-复制”算法实现的。</li>
<li><strong>可预测的停顿</strong>：这是 G1 相对于 CMS 的另一个大优势，降低停顿时间是 G1 和 CMS 共同的关注点，但 G1 除了追求低停顿外，还能建立可预测的停顿时间模型，能让使用者明确指定在一个长度为 M 毫秒的时间片段内。</li>
</ul>
<p>G1 收集器的运作大致分为以下几个步骤：</p>
<ul>
<li><strong>初始标记</strong></li>
<li><strong>并发标记</strong></li>
<li><strong>最终标记</strong></li>
<li><strong>筛选回收</strong></li>
</ul>
<p><strong>G1 收集器在后台维护了一个优先列表，每次根据允许的收集时间，优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)</strong> 。</p>
<hr>
<h5 id="ZGC-收集器"><a href="#ZGC-收集器" class="headerlink" title="ZGC 收集器"></a>ZGC 收集器</h5><p>与 CMS 中的 ParNew 和 G1 类似，ZGC 也采用标记-复制算法，不过 ZGC 对该算法做了重大改进。</p>
<p>在 ZGC 中出现 Stop The World 的情况会更少！</p>
<hr>
<h3 id="类文件结构详解"><a href="#类文件结构详解" class="headerlink" title="类文件结构详解"></a>类文件结构详解</h3><h4 id="一-概述"><a href="#一-概述" class="headerlink" title="一 概述"></a>一 概述</h4><p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/bg/desktop%E7%B1%BB%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84%E6%A6%82%E8%A7%88.png" alt="java虚拟机"></p>
<p><code>.class</code>文件是不同的语言在 Java 虚拟机之间的重要桥梁，同时也是支持 Java 跨平台很重要的一个原因</p>
<hr>
<h4 id="二-Class-文件结构总结"><a href="#二-Class-文件结构总结" class="headerlink" title="二 Class 文件结构总结"></a>二 Class 文件结构总结</h4><h5 id="2-1-魔数（Magic-Number）"><a href="#2-1-魔数（Magic-Number）" class="headerlink" title="2.1 魔数（Magic Number）"></a>2.1 魔数（Magic Number）</h5><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">u4             magic; //Class 文件的标志</span><br></pre></td></tr></table></figure>

<p>每个 Class 文件的头 4 个字节称为魔数（Magic Number）,它的唯一作用是<strong>确定这个文件是否为一个能被虚拟机接收的 Class 文件</strong>。</p>
<h5 id="2-2-Class-文件版本号（Minor-amp-Major-Version）"><a href="#2-2-Class-文件版本号（Minor-amp-Major-Version）" class="headerlink" title="2.2 Class 文件版本号（Minor&amp;Major Version）"></a>2.2 Class 文件版本号（Minor&amp;Major Version）</h5><p>紧接着魔数的四个字节存储的是 Class 文件的版本号：第 5 和第 6 位是<strong>次版本号</strong>，第 7 和第 8 位是<strong>主版本号</strong>。</p>
<h5 id="2-3-常量池（Constant-Pool）"><a href="#2-3-常量池（Constant-Pool）" class="headerlink" title="2.3 常量池（Constant Pool）"></a>2.3 常量池（Constant Pool）</h5><p>紧接着主次版本号之后的是常量池，常量池的数量是 <code>constant_pool_count-1</code>（<strong>常量池计数器是从 1 开始计数的，将第 0 项常量空出来是有特殊考虑的，索引值为 0 代表“不引用任何一个常量池项”</strong>）。</p>
<hr>
<h5 id="2-4-访问标志-Access-Flags"><a href="#2-4-访问标志-Access-Flags" class="headerlink" title="2.4 访问标志(Access Flags)"></a>2.4 访问标志(Access Flags)</h5><h5 id="2-5-当前类（This-Class）、父类（Super-Class）、接口（Interfaces）索引集合"><a href="#2-5-当前类（This-Class）、父类（Super-Class）、接口（Interfaces）索引集合" class="headerlink" title="2.5 当前类（This Class）、父类（Super Class）、接口（Interfaces）索引集合"></a>2.5 当前类（This Class）、父类（Super Class）、接口（Interfaces）索引集合</h5><h5 id="2-6-字段表集合（Fields）"><a href="#2-6-字段表集合（Fields）" class="headerlink" title="2.6 字段表集合（Fields）"></a>2.6 字段表集合（Fields）</h5><h5 id="2-7-方法表集合（Methods）"><a href="#2-7-方法表集合（Methods）" class="headerlink" title="2.7 方法表集合（Methods）"></a>2.7 方法表集合（Methods）</h5><p>methods_count 表示方法的数量，而 method_info 表示方法表。</p>
<p>注意：因为<code>volatile</code>修饰符和<code>transient</code>修饰符不可以修饰方法，所以方法表的访问标志中没有这两个对应的标志，但是增加了<code>synchronized</code>、<code>native</code>、<code>abstract</code>等关键字修饰方法，所以也就多了这些关键字对应的标志。</p>
<hr>
<h5 id="2-8-属性表集合（Attributes）"><a href="#2-8-属性表集合（Attributes）" class="headerlink" title="2.8 属性表集合（Attributes）"></a>2.8 属性表集合（Attributes）</h5><hr>
<h3 id="类加载过程详解"><a href="#类加载过程详解" class="headerlink" title="类加载过程详解"></a>类加载过程详解</h3><h4 id="类的生命周期"><a href="#类的生命周期" class="headerlink" title="类的生命周期"></a>类的生命周期</h4><p><img src="G:\GitDocument\Study\images\面试.assets\image-20230216161700631.png" alt="image-20230216161700631"></p>
<hr>
<h4 id="类加载过程"><a href="#类加载过程" class="headerlink" title="类加载过程"></a>类加载过程</h4><p>Class 文件需要加载到虚拟机中之后才能运行和使用，那么虚拟机是如何加载这些 Class 文件呢？</p>
<p>系统加载 Class 类型的文件主要三步：<strong>加载-&gt;连接-&gt;初始化</strong>。连接过程又可分为三步：<strong>验证-&gt;准备-&gt;解析</strong>。</p>
<p><img src="https://oss.javaguide.cn/github/javaguide/java/jvm/class-loading-procedure.png" alt="类加载过程"></p>
<hr>
<h5 id="加载"><a href="#加载" class="headerlink" title="加载"></a>加载</h5><p>类加载过程的第一步，主要完成下面 3 件事情：</p>
<ol>
<li>通过全类名获取定义此类的二进制字节流。</li>
<li>将字节流所代表的静态存储结构转换为方法区的运行时数据结构。</li>
<li>在内存中生成一个代表该类的 <code>Class</code> 对象，作为方法区这些数据的访问入口。</li>
</ol>
<p>一个非数组类的加载阶段（加载阶段获取类的二进制字节流的动作）是可控性最强的阶段，这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式（重写一个类加载器的 <code>loadClass()</code> 方法）。数组类型不通过类加载器创建，它由 Java 虚拟机直接创建。</p>
<hr>
<h5 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h5><p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/%E9%AA%8C%E8%AF%81%E9%98%B6%E6%AE%B5.png" alt="验证阶段示意图"></p>
<hr>
<h5 id="准备"><a href="#准备" class="headerlink" title="准备"></a>准备</h5><p><strong>准备阶段是正式为类变量分配内存并设置类变量初始值的阶段</strong></p>
<hr>
<h5 id="解析"><a href="#解析" class="headerlink" title="解析"></a>解析</h5><p>解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程，也就是得到类或者字段、方法在内存中的指针或者偏移量</p>
<p><img src="https://oss.javaguide.cn/github/javaguide/java/jvm/symbol-reference-and-direct-reference.png" alt="符号引用和直接引用"></p>
<hr>
<h5 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h5><p>初始化阶段是执行初始化方法 <code>&lt;clinit&gt; ()</code>方法的过程，是类加载的最后一步，这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。</p>
<h6 id="6种情况"><a href="#6种情况" class="headerlink" title="6种情况"></a>6种情况</h6><ol>
<li>当遇到 <code>new</code> 、 <code>getstatic</code>、<code>putstatic</code> 或 <code>invokestatic</code> 这 4 条直接码指令时</li>
<li>使用 <code>java.lang.reflect</code> 包的方法对类进行反射调用时如 <code>Class.forname(&quot;...&quot;)</code>, <code>newInstance()</code> 等等。</li>
<li>初始化一个类，如果其父类还未初始化，则先触发该父类的初始化。</li>
<li>当虚拟机启动时，用户需要定义一个要执行的主类 (包含 <code>main</code> 方法的那个类)，虚拟机会先初始化这个类。</li>
<li><code>MethodHandle</code> 和 <code>VarHandle</code> 可以看作是轻量级的反射调用机制，而要想使用这 2 个调用， 就必须先使用 <code>findStaticVarHandle</code> 来初始化要调用的类。</li>
<li>当一个接口中定义了 JDK8 新加入的默认方法（被 default 关键字修饰的接口方法）时，如果有这个接口的实现类发生了初始化，那该接口要在其之前被初始化。</li>
</ol>
<hr>
<h5 id="卸载"><a href="#卸载" class="headerlink" title="卸载"></a>卸载</h5><p>卸载类即该类的 Class 对象被 GC。</p>
<p>卸载类需要满足 3 个要求:</p>
<ol>
<li>该类的所有的实例对象都已被 GC，也就是说堆不存在该类的实例对象。</li>
<li>该类没有在其他任何地方被引用</li>
<li>该类的类加载器的实例已被 GC</li>
</ol>
<p>所以，在 JVM 生命周期内，由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。</p>
<hr>
<h3 id="类加载器详解"><a href="#类加载器详解" class="headerlink" title="类加载器详解"></a>类加载器详解</h3><h4 id="类加载器总结"><a href="#类加载器总结" class="headerlink" title="类加载器总结"></a>类加载器总结</h4><p>JVM 中内置了三个重要的 ClassLoader，除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自<code>java.lang.ClassLoader</code>：</p>
<ol>
<li><strong>BootstrapClassLoader(启动类加载器)</strong> ：最顶层的加载类，由 C++实现，负责加载 <code>%JAVA_HOME%/lib</code>目录下的 jar 包和类或者被 <code>-Xbootclasspath</code>参数指定的路径中的所有类。</li>
<li><strong>ExtensionClassLoader(扩展类加载器)</strong> ：主要负责加载 <code>%JRE_HOME%/lib/ext</code> 目录下的 jar 包和类，或被 <code>java.ext.dirs</code> 系统变量所指定的路径下的 jar 包。</li>
<li><strong>AppClassLoader(应用程序类加载器)</strong> ：面向我们用户的加载器，负责加载当前应用 classpath 下的所有 jar 包和类。</li>
</ol>
<hr>
<h4 id="双亲委派模型"><a href="#双亲委派模型" class="headerlink" title="双亲委派模型"></a>双亲委派模型</h4><h5 id="双亲委派模型介绍"><a href="#双亲委派模型介绍" class="headerlink" title="双亲委派模型介绍"></a>双亲委派模型介绍</h5><p>即在类加载的时候，系统会首先判断当前类是否被加载过。已经被加载的类会直接返回，否则才会尝试加载。加载的时候，首先会把该请求委派给父类加载器的 <code>loadClass()</code> 处理，因此所有的请求最终都应该传送到顶层的启动类加载器 <code>BootstrapClassLoader</code> 中。当父类加载器无法处理时，才由自己来处理。当父类加载器为 null 时，会使用启动类加载器 <code>BootstrapClassLoader</code> 作为父类加载器。</p>
<p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/classloader_WPS%E5%9B%BE%E7%89%87.png" alt="ClassLoader"></p>
<p><code>AppClassLoader</code>的父类加载器为<code>ExtClassLoader</code>， <code>ExtClassLoader</code>的父类加载器为 null，<strong>null 并不代表<code>ExtClassLoader</code>没有父类加载器，而是 <code>BootstrapClassLoader</code></strong> 。</p>
<p>其实这个双亲翻译的容易让别人误解，我们一般理解的双亲都是父母，这里的双亲更多地表达的是“父母这一辈”的人而已，并不是说真的有一个 Mother ClassLoader 和一个 Father ClassLoader 。另外，类加载器之间的“父子”关系也不是通过继承来体现的，是由“优先级”来决定。</p>
<hr>
<h5 id="双亲委派模型的好处"><a href="#双亲委派模型的好处" class="headerlink" title="双亲委派模型的好处"></a>双亲委派模型的好处</h5><p>双亲委派模型保证了 Java 程序的稳定运行，可以避免类的重复加载（JVM 区分不同类的方式不仅仅根据类名，相同的类文件被不同的类加载器加载产生的是两个不同的类），也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型，而是每个类加载器加载自己的话就会出现一些问题，比如我们编写一个称为 <code>java.lang.Object</code> 类的话，那么程序运行的时候，系统就会出现多个不同的 <code>Object</code> 类。</p>
<hr>
<h5 id="如果我们不想用双亲委派模型怎么办？"><a href="#如果我们不想用双亲委派模型怎么办？" class="headerlink" title="如果我们不想用双亲委派模型怎么办？"></a>如果我们不想用双亲委派模型怎么办？</h5><p>自定义加载器的话，需要继承 <code>ClassLoader</code> 。如果我们不想打破双亲委派模型，就重写 <code>ClassLoader</code> 类中的 <code>findClass()</code> 方法即可，无法被父类加载器加载的类最终会通过这个方法被加载。但是，如果想打破双亲委派模型则需要重写 <code>loadClass()</code> 方法</p>
<hr>
<h5 id="自定义类加载器"><a href="#自定义类加载器" class="headerlink" title="自定义类加载器"></a>自定义类加载器</h5><p>除了 <code>BootstrapClassLoader</code> 其他类加载器均由 Java 实现且全部继承自<code>java.lang.ClassLoader</code>。如果我们要自定义自己的类加载器，很明显需要继承 <code>ClassLoader</code></p>
<hr>
<h3 id="最重要的-JVM-参数总结"><a href="#最重要的-JVM-参数总结" class="headerlink" title="最重要的 JVM 参数总结"></a>最重要的 JVM 参数总结</h3><h4 id="1-概述"><a href="#1-概述" class="headerlink" title="1.概述"></a>1.概述</h4><h4 id="2-堆内存相关"><a href="#2-堆内存相关" class="headerlink" title="2.堆内存相关"></a>2.堆内存相关</h4><blockquote>
<p><strong>此内存区域的唯一目的就是存放对象实例，几乎所有的对象实例以及数组都在这里分配内存。</strong></p>
</blockquote>
<h5 id="2-1-显式指定堆内存–Xms和-Xmx"><a href="#2-1-显式指定堆内存–Xms和-Xmx" class="headerlink" title="2.1.显式指定堆内存–Xms和-Xmx"></a>2.1.显式指定堆内存<code>–Xms</code>和<code>-Xmx</code></h5><h5 id="2-2-显式新生代内存-Young-Generation"><a href="#2-2-显式新生代内存-Young-Generation" class="headerlink" title="2.2.显式新生代内存(Young Generation)"></a>2.2.显式新生代内存(Young Generation)</h5><p><strong>1.通过<code>-XX:NewSize</code>和<code>-XX:MaxNewSize</code>指定</strong></p>
<p><strong>2.通过<code>-Xmn&lt;young size&gt;[unit]</code>指定</strong></p>
<h5 id="2-3-显式指定永久代-x2F-元空间的大小"><a href="#2-3-显式指定永久代-x2F-元空间的大小" class="headerlink" title="2.3.显式指定永久代&#x2F;元空间的大小"></a>2.3.显式指定永久代&#x2F;元空间的大小</h5><p><strong>从 Java 8 开始，如果我们没有指定 Metaspace 的大小，随着更多类的创建，虚拟机会耗尽所有可用的系统内存（永久代并不会出现这种情况）。</strong></p>
<h4 id="3-垃圾收集相关"><a href="#3-垃圾收集相关" class="headerlink" title="3.垃圾收集相关"></a>3.垃圾收集相关</h4><h5 id="3-1-垃圾回收器"><a href="#3-1-垃圾回收器" class="headerlink" title="3.1.垃圾回收器"></a>3.1.垃圾回收器</h5><p>JVM 具有四种类型的 GC 实现：</p>
<ul>
<li>串行垃圾收集器</li>
<li>并行垃圾收集器</li>
<li>CMS 垃圾收集器</li>
<li>G1 垃圾收集器</li>
</ul>
<h5 id="3-2-GC-日志记录"><a href="#3-2-GC-日志记录" class="headerlink" title="3.2.GC 日志记录"></a>3.2.GC 日志记录</h5><h4 id="4-处理-OOM"><a href="#4-处理-OOM" class="headerlink" title="4.处理 OOM"></a>4.处理 OOM</h4><h3 id="大白话带你认识JVM"><a href="#大白话带你认识JVM" class="headerlink" title="大白话带你认识JVM"></a>大白话带你认识JVM</h3><p>JVM其实就类似于一台小电脑运行在windows或者linux这些操作系统环境下即可。它直接和操作系统进行交互，与硬件不直接交互，而操作系统可以帮我们完成和硬件进行交互的工作。</p>
<h4 id="一、JVM的基本介绍"><a href="#一、JVM的基本介绍" class="headerlink" title="一、JVM的基本介绍"></a>一、JVM的基本介绍</h4><h5 id="1-1-Java文件是如何被运行的"><a href="#1-1-Java文件是如何被运行的" class="headerlink" title="1.1 Java文件是如何被运行的"></a>1.1 Java文件是如何被运行的</h5><h6 id="①-类加载器"><a href="#①-类加载器" class="headerlink" title="① 类加载器"></a>① 类加载器</h6><p>如果 <strong>JVM</strong> 想要执行这个 <strong>.class</strong> 文件，我们需要将其装进一个 <strong>类加载器</strong> 中，它就像一个搬运工一样，会把所有的 <strong>.class</strong> 文件全部搬进JVM里面来。</p>
<p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/81f1813f371c40ffa1c1f6d78bc49ed9-new-image28314ec8-066f-451e-8373-4517917d6bf7.png" alt="img"></p>
<h6 id="②-方法区"><a href="#②-方法区" class="headerlink" title="② 方法区"></a>② 方法区</h6><p><strong>方法区</strong> 是用于存放类似于元数据信息方面的数据的，比如类信息，常量，静态变量，编译后代码···等</p>
<h6 id="③-堆"><a href="#③-堆" class="headerlink" title="③ 堆"></a>③ 堆</h6><p><strong>堆</strong> 主要放了一些存储的数据，比如对象实例，数组···等，它和方法区都同属于 <strong>线程共享区域</strong> 。也就是说它们都是 <strong>线程不安全</strong> 的</p>
<h6 id="④-栈"><a href="#④-栈" class="headerlink" title="④ 栈"></a>④ 栈</h6><p><strong>栈</strong> 这是我们的代码运行空间。我们编写的每一个方法都会放到 <strong>栈</strong> 里面运行。</p>
<h6 id="⑤-程序计数器"><a href="#⑤-程序计数器" class="headerlink" title="⑤ 程序计数器"></a>⑤ 程序计数器</h6><p>主要就是完成一个加载工作，类似于一个指针一样的，指向下一行我们需要执行的代码。和栈一样，都是 <strong>线程独享</strong> 的，就是说每一个线程都会有自己对应的一块区域而不会存在并发和多线程的问题。</p>
<h6 id="小总结"><a href="#小总结" class="headerlink" title="小总结"></a>小总结</h6><ol>
<li>Java文件经过编译后变成 .class 字节码文件</li>
<li>字节码文件通过类加载器被搬运到 JVM 虚拟机中</li>
<li>虚拟机主要的5大块：方法区，堆都为线程共享区域，有线程安全问题，栈和本地方法栈和计数器都是独享区域，不存在线程安全问题，而 JVM 的调优主要就是围绕堆，栈两大块进行</li>
</ol>
<hr>
<h4 id="二、类加载器的介绍"><a href="#二、类加载器的介绍" class="headerlink" title="二、类加载器的介绍"></a>二、类加载器的介绍</h4><p>之前也提到了它是负责加载.class文件的，它们在文件开头会有特定的文件标示，将class文件字节码内容加载到内存中，并将这些内容转换成方法区中的运行时数据结构，并且ClassLoader只负责class文件的加载，而是否能够运行则由 Execution Engine 来决定</p>
<hr>
<h5 id="2-1-类加载器的流程"><a href="#2-1-类加载器的流程" class="headerlink" title="2.1 类加载器的流程"></a>2.1 类加载器的流程</h5><p>从类被加载到虚拟机内存中开始，到释放内存总共有7个步骤：加载，验证，准备，解析，初始化，使用，卸载。其中<strong>验证，准备，解析三个部分统称为连接</strong></p>
<h6 id="2-1-1-加载"><a href="#2-1-1-加载" class="headerlink" title="2.1.1 加载"></a>2.1.1 加载</h6><ol>
<li>将class文件加载到内存</li>
<li>将静态数据结构转化成方法区中运行时的数据结构</li>
<li>在堆中生成一个代表这个类的 java.lang.Class对象作为数据访问的入口</li>
</ol>
<h6 id="2-1-2-链接"><a href="#2-1-2-链接" class="headerlink" title="2.1.2 链接"></a>2.1.2 链接</h6><h6 id="验证：确保加载的类符合-JVM-规范和安全，保证被校验类的方法在运行时不会做出危害虚拟机的事件，其实就是一个安全检查"><a href="#验证：确保加载的类符合-JVM-规范和安全，保证被校验类的方法在运行时不会做出危害虚拟机的事件，其实就是一个安全检查" class="headerlink" title="验证：确保加载的类符合 JVM 规范和安全，保证被校验类的方法在运行时不会做出危害虚拟机的事件，其实就是一个安全检查"></a>验证：确保加载的类符合 JVM 规范和安全，保证被校验类的方法在运行时不会做出危害虚拟机的事件，其实就是一个安全检查</h6><p>准备：为static变量在方法区中分配内存空间，设置变量的初始值，例如 static int a &#x3D; 3 （注意：准备阶段只设置类中的静态变量（方法区中），不包括实例变量（堆内存中），实例变量是对象初始化时赋值的）</p>
<p>解析：虚拟机将常量池内的符号引用替换为直接引用的过程（符号引用比如我现在import java.util.ArrayList这就算符号引用，直接引用就是指针或者对象地址，注意引用对象一定是在内存进行）</p>
<hr>
<h6 id="2-1-3-初始化"><a href="#2-1-3-初始化" class="headerlink" title="2.1.3 初始化"></a>2.1.3 初始化</h6><p>初始化其实就是执行类构造器方法的<code>&lt;clinit&gt;()</code>的过程，而且要保证执行前父类的<code>&lt;clinit&gt;()</code>方法执行完毕。这个方法由编译器收集，顺序执行所有类变量（static修饰的成员变量）显式初始化和静态代码块中语句。此时准备阶段时的那个 <code>static int a</code> 由默认初始化的0变成了显式初始化的3。 由于执行顺序缘故，初始化阶段类变量如果在静态代码块中又进行了更改，会覆盖类变量的显式初始化，最终值会为静态代码块中的赋值。</p>
<hr>
<h6 id="2-1-4-卸载"><a href="#2-1-4-卸载" class="headerlink" title="2.1.4 卸载"></a>2.1.4 卸载</h6><p>GC将无用对象从内存中卸载</p>
<hr>
<h5 id="2-2-类加载器的加载顺序"><a href="#2-2-类加载器的加载顺序" class="headerlink" title="2.2 类加载器的加载顺序"></a>2.2 类加载器的加载顺序</h5><p>加载一个Class类的顺序也是有优先级的，类加载器从最底层开始往上的顺序是这样的</p>
<ol>
<li>BootStrap ClassLoader：rt.jar</li>
<li>Extension ClassLoader: 加载扩展的jar包</li>
<li>App ClassLoader：指定的classpath下面的jar包</li>
<li>Custom ClassLoader：自定义的类加载器</li>
</ol>
<hr>
<h5 id="2-3-双亲委派机制"><a href="#2-3-双亲委派机制" class="headerlink" title="2.3 双亲委派机制"></a>2.3 双亲委派机制</h5><p>当一个类收到了加载请求时，它是不会先自己去尝试加载的，而是委派给父类去完成，比如我现在要 new 一个 Person，这个 Person 是我们自定义的类，如果我们要加载它，就会先委派 App ClassLoader ，只有当父类加载器都反馈自己无法完成这个请求（也就是父类加载器都没有找到加载所需的 Class）时，子类加载器才会自行尝试加载。</p>
<hr>
<h4 id="三、运行时数据区"><a href="#三、运行时数据区" class="headerlink" title="三、运行时数据区"></a>三、运行时数据区</h4><h5 id="3-1-本地方法栈和程序计数器"><a href="#3-1-本地方法栈和程序计数器" class="headerlink" title="3.1 本地方法栈和程序计数器"></a>3.1 本地方法栈和程序计数器</h5><p>比如说我们现在点开Thread类的源码，会看到它的start0方法带有一个native关键字修饰，而且不存在方法体，这种用native修饰的方法就是本地方法，这是使用C来实现的，然后一般这些方法都会放到一个叫做本地方法栈的区域。</p>
<p>程序计数器其实就是一个指针，它指向了我们程序中下一句需要执行的指令，它也是内存区域中唯一一个不会出现OutOfMemoryError的区域，而且占用内存空间小到基本可以忽略不计。这个内存仅代表当前线程所执行的字节码的行号指示器，字节码解析器通过改变这个计数器的值选取下一条需要执行的字节码指令。</p>
<p>如果执行的是native方法，那这个指针就不工作了。</p>
<hr>
<h5 id="3-2-方法区"><a href="#3-2-方法区" class="headerlink" title="3.2 方法区"></a>3.2 方法区</h5><p>方法区主要的作用是存放类的元数据信息，常量和静态变量···等。当它存储的信息过大时，会在无法满足内存分配时报错。</p>
<hr>
<h5 id="3-3-虚拟机栈和虚拟机堆"><a href="#3-3-虚拟机栈和虚拟机堆" class="headerlink" title="3.3 虚拟机栈和虚拟机堆"></a>3.3 虚拟机栈和虚拟机堆</h5><p>一句话便是：&#x3D;&#x3D;栈管运行，堆管存储&#x3D;&#x3D;。则虚拟机栈负责运行代码，而虚拟机堆负责存储数据。</p>
<hr>
<h6 id="3-3-1-虚拟机栈的概念"><a href="#3-3-1-虚拟机栈的概念" class="headerlink" title="3.3.1 虚拟机栈的概念"></a>3.3.1 虚拟机栈的概念</h6><p>它是Java方法执行的内存模型。里面会对局部变量，动态链表，方法出口，栈的操作（入栈和出栈）进行存储，且线程独享。同时如果我们听到<strong>局部变量表</strong>，那也是在说虚拟机栈</p>
<hr>
<h6 id="3-3-2-虚拟机栈存在的异常"><a href="#3-3-2-虚拟机栈存在的异常" class="headerlink" title="3.3.2 虚拟机栈存在的异常"></a>3.3.2 虚拟机栈存在的异常</h6><p>如果线程请求的栈的深度大于虚拟机栈的最大深度，就会报 <strong>StackOverflowError</strong> （这种错误经常出现在递归中）。Java虚拟机也可以动态扩展，但随着扩展会不断地申请内存，当无法申请足够内存时就会报错 <strong>OutOfMemoryError</strong>。</p>
<hr>
<h6 id="3-3-3-虚拟机栈的生命周期"><a href="#3-3-3-虚拟机栈的生命周期" class="headerlink" title="3.3.3 虚拟机栈的生命周期"></a>3.3.3 虚拟机栈的生命周期</h6><p>对于栈来说，不存在垃圾回收。只要程序运行结束，栈的空间自然就会释放了。栈的生命周期和所处的线程是一致的。</p>
<p>这里补充一句：8种基本类型的变量+对象的引用变量+实例方法都是在栈里面分配内存。</p>
<hr>
<h6 id="3-3-4-虚拟机栈的执行"><a href="#3-3-4-虚拟机栈的执行" class="headerlink" title="3.3.4 虚拟机栈的执行"></a>3.3.4 虚拟机栈的执行</h6><p>我们经常说的栈帧数据，说白了在JVM中叫栈帧，放到Java中其实就是方法，它也是存放在栈中的。</p>
<p>栈中的数据都是以栈帧的格式存在，它是一个关于方法和运行期数据的数据集。比如我们执行一个方法a，就会对应产生一个栈帧A1，然后A1会被压入栈中。同理方法b会有一个B1，方法c会有一个C1，等到这个线程执行完毕后，栈会先弹出C1，后B1,A1。它是一个先进后出，后进先出原则</p>
<hr>
<h6 id="3-3-5-局部变量的复用"><a href="#3-3-5-局部变量的复用" class="headerlink" title="3.3.5 局部变量的复用"></a>3.3.5 局部变量的复用</h6><p>局部变量表用于存放方法参数和方法内部所定义的局部变量。它的容量是以Slot为最小单位，一个slot可以存放32位以内的数据类型。</p>
<hr>
<h6 id="3-3-6-虚拟机堆的概念"><a href="#3-3-6-虚拟机堆的概念" class="headerlink" title="3.3.6 虚拟机堆的概念"></a>3.3.6 虚拟机堆的概念</h6><p>JVM内存会划分为堆内存和非堆内存，堆内存中也会划分为<strong>年轻代</strong>和<strong>老年代</strong>，而非堆内存则为<strong>永久代</strong>。年轻代又会分为<strong>Eden</strong>和<strong>Survivor</strong>区。Survivor也会分为<strong>FromPlace</strong>和<strong>ToPlace</strong>，toPlace的survivor区域是空的。Eden，FromPlace和ToPlace的默认占比为 <strong>8:1:1</strong>。</p>
<hr>
<h6 id="3-3-7-Eden年轻代的介绍"><a href="#3-3-7-Eden年轻代的介绍" class="headerlink" title="3.3.7 Eden年轻代的介绍"></a>3.3.7 Eden年轻代的介绍</h6><hr>
<h6 id="3-3-8-如何判断一个对象需要被干掉"><a href="#3-3-8-如何判断一个对象需要被干掉" class="headerlink" title="3.3.8 如何判断一个对象需要被干掉"></a>3.3.8 如何判断一个对象需要被干掉</h6><hr>
<h6 id="3-3-9-如何宣告一个对象的真正死亡"><a href="#3-3-9-如何宣告一个对象的真正死亡" class="headerlink" title="3.3.9 如何宣告一个对象的真正死亡"></a>3.3.9 如何宣告一个对象的真正死亡</h6><p>判断一个对象的死亡至少需要两次标记</p>
<ol>
<li>如果对象进行可达性分析之后没发现与GC Roots相连的引用链，那它将会第一次标记并且进行一次筛选。判断的条件是决定这个对象是否有必要执行finalize()方法。如果对象有必要执行finalize()方法，则被放入F-Queue队列中。</li>
<li>GC对F-Queue队列中的对象进行二次标记。如果对象在finalize()方法中重新与引用链上的任何一个对象建立了关联，那么二次标记时则会将它移出“即将回收”集合。如果此时对象还没成功逃脱，那么只能被回收了。</li>
</ol>
<p>如果确定对象已经死亡，我们又该如何回收这些垃圾呢</p>
<hr>
<h5 id="3-4-垃圾回收算法"><a href="#3-4-垃圾回收算法" class="headerlink" title="3.4 垃圾回收算法"></a>3.4 垃圾回收算法</h5><h6 id="3-4-1-标记清除算法"><a href="#3-4-1-标记清除算法" class="headerlink" title="3.4.1 标记清除算法"></a>3.4.1 标记清除算法</h6><p>标记清除算法就是分为“标记”和“清除”两个阶段。标记出所有需要回收的对象，标记结束后统一回收。这个套路很简单，也存在不足，后续的算法都是根据这个基础来加以改进的。</p>
<hr>
<h6 id="3-4-2-复制算法"><a href="#3-4-2-复制算法" class="headerlink" title="3.4.2 复制算法"></a>3.4.2 复制算法</h6><hr>
<h6 id="3-4-3-标记整理算法"><a href="#3-4-3-标记整理算法" class="headerlink" title="3.4.3 标记整理算法"></a>3.4.3 标记整理算法</h6><hr>
<h6 id="3-4-4-分代收集算法"><a href="#3-4-4-分代收集算法" class="headerlink" title="3.4.4 分代收集算法"></a>3.4.4 分代收集算法</h6><hr>
<h3 id="JDK-可视化分析工具"><a href="#JDK-可视化分析工具" class="headerlink" title="JDK 可视化分析工具"></a>JDK 可视化分析工具</h3><hr>
<h2 id="新特性"><a href="#新特性" class="headerlink" title="新特性"></a>新特性</h2><h3 id="Java8-新特性实战"><a href="#Java8-新特性实战" class="headerlink" title="Java8 新特性实战"></a>Java8 新特性实战</h3><h4 id="Interface"><a href="#Interface" class="headerlink" title="Interface"></a>Interface</h4><hr>
<h4 id="functional-interface-函数式接口"><a href="#functional-interface-函数式接口" class="headerlink" title="functional interface 函数式接口"></a>functional interface 函数式接口</h4><hr>
<h4 id="Lambda-表达式-1"><a href="#Lambda-表达式-1" class="headerlink" title="Lambda 表达式"></a>Lambda 表达式</h4><hr>
<h4 id="Stream"><a href="#Stream" class="headerlink" title="Stream"></a>Stream</h4><hr>
<h4 id="Optional"><a href="#Optional" class="headerlink" title="Optional"></a>Optional</h4><hr>
<h4 id="Date-Time-API"><a href="#Date-Time-API" class="headerlink" title="Date-Time API"></a>Date-Time API</h4><hr>
<h1 id="计算机基础"><a href="#计算机基础" class="headerlink" title="计算机基础"></a>计算机基础</h1><h2 id="网络"><a href="#网络" class="headerlink" title="网络"></a>网络</h2><h3 id="计算机网络常见面试题总结"><a href="#计算机网络常见面试题总结" class="headerlink" title="计算机网络常见面试题总结"></a>计算机网络常见面试题总结</h3><h4 id="计算机网络基础"><a href="#计算机网络基础" class="headerlink" title="计算机网络基础"></a>计算机网络基础</h4><h5 id="OSI-和-TCP-x2F-IP-网络分层模型"><a href="#OSI-和-TCP-x2F-IP-网络分层模型" class="headerlink" title="OSI 和 TCP&#x2F;IP 网络分层模型"></a>OSI 和 TCP&#x2F;IP 网络分层模型</h5><ul>
<li>OSI 七层模型是什么？每一层的作用是什么？</li>
<li>TCP&#x2F;IP 四层模型是什么？每一层的作用是什么？</li>
<li>为什么网络要分层？</li>
</ul>
<h6 id="OSI-和-TCP-x2F-IP-网络分层模型详解（基础）"><a href="#OSI-和-TCP-x2F-IP-网络分层模型详解（基础）" class="headerlink" title="OSI 和 TCP&#x2F;IP 网络分层模型详解（基础）"></a>OSI 和 TCP&#x2F;IP 网络分层模型详解（基础）</h6><p>&#x3D;&#x3D;<strong>OSI 七层模型</strong>&#x3D;&#x3D;</p>
<p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/cs-basics/network/osi-7-model.png" alt="OSI 七层模型"></p>
<p>每一层都专注做一件事情，并且每一层都需要使用下一层提供的功能</p>
<p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/cs-basics/network/osi-model-detail.png" alt="img"></p>
<p>&#x3D;&#x3D;<strong>TCP&#x2F;IP 四层模型</strong>&#x3D;&#x3D;</p>
<p><strong>TCP&#x2F;IP 四层模型</strong> 是目前被广泛采用的一种模型,我们可以将 TCP &#x2F; IP 模型看作是 OSI 七层模型的精简版本，由以下 4 层组成：</p>
<ol>
<li>应用层</li>
<li>传输层</li>
<li>网络层</li>
<li>网络接口层</li>
</ol>
<p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/cs-basics/network/tcp-ip-4-model.png" alt="TCP/IP 四层模型"></p>
<hr>
<p>应用层（Application layer）</p>
<p><strong>应用层位于传输层之上，主要提供两个终端设备上的应用程序之间信息交换的服务，它定义了信息交换的格式，消息会交给下一层传输层来传输</strong></p>
<hr>
<p> 传输层（Transport layer）</p>
<p><strong>传输层的主要任务就是负责向两台终端设备进程之间的通信提供通用的数据传输服务。</strong></p>
<p><strong>运输层主要使用以下两种协议：</strong></p>
<ol>
<li><strong>传输控制协议 TCP</strong>（Transmisson Control Protocol）–提供 <strong>面向连接</strong> 的，<strong>可靠的</strong> 数据传输服务。</li>
<li><strong>用户数据协议 UDP</strong>（User Datagram Protocol）–提供 <strong>无连接</strong> 的，尽最大努力的数据传输服务（不保证数据传输的可靠性）。</li>
</ol>
<hr>
<p>网络层（Network layer）</p>
<p><strong>网络层负责为分组交换网上的不同主机提供通信服务。</strong> </p>
<p><strong>不要把运输层的“用户数据报 UDP”和网络层的“IP 数据报”弄混</strong>。</p>
<p><strong>网络层的还有一个任务就是选择合适的路由，使源主机运输层所传下来的分组，能通过网络层中的路由器找到目的主机。</strong></p>
<hr>
<p>网络接口层（Network interface layer）</p>
<p>数据链路层(data link layer)通常简称为链路层（ 两台主机之间的数据传输，总是在一段一段的链路上传送的）。<strong>数据链路层的作用是将网络层交下来的 IP 数据报组装成帧，在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息（如同步信息，地址信息，差错控制等）。</strong></p>
<p><strong>物理层的作用是实现相邻计算机节点之间比特流的透明传送，尽可能屏蔽掉具体传输介质和物理设备的差异</strong></p>
<hr>
<h6 id="网络分层的原因"><a href="#网络分层的原因" class="headerlink" title="网络分层的原因"></a>网络分层的原因</h6><p><strong>各层之间相互独立</strong>：各层之间相互独立，各层之间不需要关心其他层是如何实现的，只需要知道自己如何调用下层提供好的功能就可以了（可以简单理解为接口调用）<strong>。这个和我们对开发时系统进行分层是一个道理。</strong></p>
<p><strong>提高了整体灵活性</strong> ：每一层都可以使用最适合的技术来实现，你只需要保证你提供的功能以及暴露的接口的规则没有改变就行了。<strong>这个和我们平时开发系统的时候要求的高内聚、低耦合的原则也是可以对应上的。</strong></p>
<p><strong>大问题化小</strong> ： 分层可以将复杂的网络问题分解为许多比较小的、界线比较清晰简单的小问题来处理和解决。这样使得复杂的计算机网络系统变得易于设计，实现和标准化。 <strong>这个和我们平时开发的时候，一般会将系统功能分解，然后将复杂的问题分解为容易理解的更小的问题是相对应的，这些较小的问题具有更好的边界（目标和接口）定义。</strong></p>
<hr>
<p><img src="https://oss.javaguide.cn/github/javaguide/cs-basics/network/network-protocol-overview.png" alt="TCP/IP 各层协议概览"></p>
<h3 id="应用层有哪些常见的协议？"><a href="#应用层有哪些常见的协议？" class="headerlink" title="应用层有哪些常见的协议？"></a>应用层有哪些常见的协议？</h3><h4 id="HTTP-超文本传输协议"><a href="#HTTP-超文本传输协议" class="headerlink" title="HTTP:超文本传输协议"></a>HTTP:超文本传输协议</h4><p><strong>超文本传输协议（HTTP，HyperText Transfer Protocol)</strong> 主要是为 Web 浏览器与 Web 服务器之间的通信而设计的。当我们使用浏览器浏览网页的时候，我们网页就是通过 HTTP 请求进行加载的，整个过程如下图所示。</p>
<hr>
<h4 id="SMTP-简单邮件传输-发送-协议"><a href="#SMTP-简单邮件传输-发送-协议" class="headerlink" title="SMTP:简单邮件传输(发送)协议"></a>SMTP:简单邮件传输(发送)协议</h4><p><strong>简单邮件传输(发送)协议（SMTP，Simple Mail Transfer Protocol）</strong> 基于 TCP 协议，用来发送电子邮件。</p>
<p>注意⚠️：<strong>接受邮件的协议不是 SMTP 而是 POP3 协议。</strong></p>
<hr>
<h4 id="POP3-x2F-IMAP-邮件接收的协议"><a href="#POP3-x2F-IMAP-邮件接收的协议" class="headerlink" title="POP3&#x2F;IMAP:邮件接收的协议"></a>POP3&#x2F;IMAP:邮件接收的协议</h4><p><strong>SMTP 协议只负责邮件的发送，真正负责接收的协议是POP3&#x2F;IMAP。</strong></p>
<hr>
<h4 id="FTP-文件传输协议"><a href="#FTP-文件传输协议" class="headerlink" title="FTP:文件传输协议"></a>FTP:文件传输协议</h4><hr>
<h4 id="Telnet-远程登陆协议"><a href="#Telnet-远程登陆协议" class="headerlink" title="Telnet:远程登陆协议"></a>Telnet:远程登陆协议</h4><p><strong>Telnet 协议</strong> 通过一个终端登陆到其他服务器，建立在可靠的传输协议 TCP 之上。Telnet 协议的最大缺点之一是所有数据（包括用户名和密码）均以明文形式发送，这有潜在的安全风险。</p>
<hr>
<h4 id="SSH-安全的网络传输协议"><a href="#SSH-安全的网络传输协议" class="headerlink" title="SSH:安全的网络传输协议"></a>SSH:安全的网络传输协议</h4><p><strong>Telnet 和 SSH 之间的主要区别在于 SSH 协议会对传输的数据进行加密保证数据安全性。</strong></p>
<hr>
<h2 id="TCP-与-UDP"><a href="#TCP-与-UDP" class="headerlink" title="TCP 与 UDP"></a>TCP 与 UDP</h2><h3 id="TCP-与-UDP-的区别（重要）"><a href="#TCP-与-UDP-的区别（重要）" class="headerlink" title="TCP 与 UDP 的区别（重要）"></a>TCP 与 UDP 的区别（重要）</h3><ol>
<li><strong>是否面向连接</strong> ：UDP 在传送数据之前不需要先建立连接。而 TCP 提供面向连接的服务，在传送数据之前必须先建立连接，数据传送结束后要释放连接。</li>
<li><strong>是否是可靠传输</strong>：远地主机在收到 UDP 报文后，不需要给出任何确认，并且不保证数据不丢失，不保证是否顺序到达。TCP 提供可靠的传输服务，TCP 在传递数据之前，会有三次握手来建立连接，而且在数据传递时，有确认、窗口、重传、拥塞控制机制。通过 TCP 连接传输的数据，无差错、不丢失、不重复、并且按序到达。</li>
<li><strong>是否有状态</strong> ：这个和上面的“是否可靠传输”相对应。TCP 传输是有状态的，这个有状态说的是 TCP 会去记录自己发送消息的状态比如消息是否发送了、是否被接收了等等。为此 ，TCP 需要维持复杂的连接状态表。而 UDP 是无状态服务，简单来说就是不管发出去之后的事情了（<strong>这很渣男！</strong>）。</li>
<li><strong>传输效率</strong> ：由于使用 TCP 进行传输的时候多了连接、确认、重传等机制，所以 TCP 的传输效率要比 UDP 低很多。</li>
<li><strong>传输形式</strong> ： TCP 是面向字节流的，UDP 是面向报文的。</li>
<li><strong>首部开销</strong> ：TCP 首部开销（20 ～ 60 字节）比 UDP 首部开销（8 字节）要大。</li>
<li><strong>是否提供广播或多播服务</strong> ：TCP 只支持点对点通信，UDP 支持一对一、一对多、多对一、多对多；</li>
</ol>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230224112955680.png" alt="image-20230224112955680"></p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230328105440470.png" alt="image-20230328105440470"></p>
<h3 id="什么时候选择-TCP-什么时候选-UDP"><a href="#什么时候选择-TCP-什么时候选-UDP" class="headerlink" title="什么时候选择 TCP,什么时候选 UDP?"></a>什么时候选择 TCP,什么时候选 UDP?</h3><p><strong>UDP 一般用于即时通信</strong>，比如： 语音、 视频 、直播等等。这些场景对传输数据的准确性要求不是特别高，比如你看视频即使少个一两帧，实际给人的感觉区别也不大。</p>
<p><strong>TCP 用于对传输准确性要求特别高的场景</strong>，比如文件传输、发送和接收邮件、远程登录等等。</p>
<hr>
<h3 id="HTTP-基于-TCP-还是-UDP？"><a href="#HTTP-基于-TCP-还是-UDP？" class="headerlink" title="HTTP 基于 TCP 还是 UDP？"></a>HTTP 基于 TCP 还是 UDP？</h3><p>🐛 修正（参见 <a target="_blank" rel="noopener" href="https://github.com/Snailclimb/JavaGuide/issues/1915">issue#1915open in new window</a>）：HTTP 3.0 之前是基于 TCP 协议的，而 HTTP3.0 将弃用 TCP，改用 <strong>基于 UDP 的 QUIC 协议</strong> 。此变化主要为了解决 HTTP&#x2F;2 中存在的队头阻塞问题。由于 HTTP&#x2F;2 在单个 TCP 连接上使用了多路复用，受到 TCP 拥塞控制的影响，少量的丢包就可能导致整个 TCP 连接上的所有流被阻塞。</p>
<hr>
<h3 id="使用-TCP-的协议有哪些-使用-UDP-的协议有哪些"><a href="#使用-TCP-的协议有哪些-使用-UDP-的协议有哪些" class="headerlink" title="使用 TCP 的协议有哪些?使用 UDP 的协议有哪些?"></a>使用 TCP 的协议有哪些?使用 UDP 的协议有哪些?</h3><p><strong>运行于 TCP 协议之上的协议</strong> ：</p>
<ol>
<li><strong>HTTP 协议</strong> ：超文本传输协议（HTTP，HyperText Transfer Protocol)主要是为 Web 浏览器与 Web 服务器之间的通信而设计的。当我们使用浏览器浏览网页的时候，我们网页就是通过 HTTP 请求进行加载的。</li>
<li><strong>HTTPS 协议</strong> ：更安全的超文本传输协议(HTTPS,Hypertext Transfer Protocol Secure)，身披 SSL 外衣的 HTTP 协议</li>
<li><strong>FTP 协议</strong>：文件传输协议 FTP（File Transfer Protocol），提供文件传输服务，<strong>基于 TCP</strong> 实现可靠的传输。使用 FTP 传输文件的好处是可以屏蔽操作系统和文件存储方式。</li>
<li><strong>SMTP 协议</strong>：简单邮件传输协议（SMTP，Simple Mail Transfer Protocol）的缩写，<strong>基于 TCP 协议</strong>，用来发送电子邮件。注意 ⚠️：接受邮件的协议不是 SMTP 而是 POP3 协议。</li>
<li><strong>POP3&#x2F;IMAP 协议</strong>： POP3 和 IMAP 两者都是负责邮件接收的协议。</li>
<li><strong>Telnet 协议</strong>：远程登陆协议，通过一个终端登陆到其他服务器。被一种称为 SSH 的非常安全的协议所取代。</li>
<li><strong>SSH 协议</strong> : SSH（ Secure Shell）是目前较可靠，专为远程登录会话和其他网络服务提供安全性的协议。利用 SSH 协议可以有效防止远程管理过程中的信息泄露问题。SSH 建立在可靠的传输协议 TCP 之上。</li>
</ol>
<p><strong>运行于 UDP 协议之上的协议</strong> ：</p>
<ol>
<li><strong>DHCP 协议</strong>：动态主机配置协议，动态配置 IP 地址</li>
<li><strong>DNS</strong> ： <strong>域名系统（DNS，Domain Name System）将人类可读的域名 (例如，<a target="_blank" rel="noopener" href="http://www.baidu.com/">www.baidu.com</a>) 转换为机器可读的 IP 地址 (例如，220.181.38.148)。</strong> 我们可以将其理解为专为互联网设计的电话薄。实际上 DNS 同时支持 UDP 和 TCP 协议。</li>
</ol>
<h3 id="x3D-x3D-TCP-三次握手和四次挥手（非常重要）-x3D-x3D"><a href="#x3D-x3D-TCP-三次握手和四次挥手（非常重要）-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;TCP 三次握手和四次挥手（非常重要）&#x3D;&#x3D;"></a>&#x3D;&#x3D;TCP 三次握手和四次挥手（非常重要）&#x3D;&#x3D;</h3><p><strong>相关面试题</strong> ：</p>
<ul>
<li>为什么要三次握手?</li>
<li>第 2 次握手传回了ACK，为什么还要传回SYN？</li>
<li>为什么要四次挥手？</li>
<li>为什么不能把服务器发送的 ACK 和 FIN 合并起来，变成三次挥手？</li>
<li>如果第二次挥手时服务器的 ACK 没有送达客户端，会怎样？</li>
<li>为什么第四次挥手客户端需要等待 2*MSL（报文段最长寿命）时间后才进入 CLOSED 状态？</li>
</ul>
<h4 id="建立连接-TCP-三次握手"><a href="#建立连接-TCP-三次握手" class="headerlink" title="建立连接-TCP 三次握手"></a>建立连接-TCP 三次握手</h4><p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/cs-basics/network/tcp-shakes-hands-three-times.png" alt="TCP 三次握手图解"></p>
<p>建立一个 TCP 连接需要“三次握手”，缺一不可 ：</p>
<ul>
<li><strong>一次握手</strong>:客户端发送带有 SYN（SEQ&#x3D;x） 标志的数据包 -&gt; 服务端，然后客户端进入 <strong>SYN_SEND</strong> 状态，等待服务器的确认；</li>
<li><strong>二次握手</strong>:服务端发送带有 SYN+ACK(SEQ&#x3D;y,ACK&#x3D;x+1) 标志的数据包 –&gt; 客户端,然后服务端进入 <strong>SYN_RECV</strong> 状态</li>
<li><strong>三次握手</strong>:客户端发送带有 ACK(ACK&#x3D;y+1) 标志的数据包 –&gt; 服务端，然后客户端和服务器端都进入<strong>ESTABLISHED</strong> 状态，完成TCP三次握手。</li>
</ul>
<p><strong>当建立了 3 次握手之后，客户端和服务端就可以传输数据啦！</strong></p>
<h5 id="为什么要三次握手"><a href="#为什么要三次握手" class="headerlink" title="为什么要三次握手?"></a>为什么要三次握手?</h5><p>三次握手的目的是建立可靠的通信信道，说到通讯，简单来说就是数据的发送与接收，而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。</p>
<ol>
<li><strong>第一次握手</strong> ：Client 什么都不能确认；Server 确认了对方发送正常，自己接收正常</li>
<li><strong>第二次握手</strong> ：Client 确认了：自己发送、接收正常，对方发送、接收正常；Server 确认了：对方发送正常，自己接收正常</li>
<li><strong>第三次握手</strong> ：Client 确认了：自己发送、接收正常，对方发送、接收正常；Server 确认了：自己发送、接收正常，对方发送、接收正常</li>
</ol>
<h5 id="四次握手，为何不进行四次握手"><a href="#四次握手，为何不进行四次握手" class="headerlink" title="四次握手，为何不进行四次握手"></a>四次握手，为何不进行四次握手</h5><ol>
<li>A 发送SYN 报文给B，这是第一次报文交互。</li>
<li>B发送ACK确认A的SYN报文，这是第二次报文交互</li>
<li>B发送自己的SYN报文给A，这是第三次报文交互</li>
<li>A需要ACK确认B的SYN报文，这是第四次报文交互</li>
</ol>
<p>原因：报文2、3为分开发送增加了延迟，同时浪费了网络的带宽</p>
<hr>
<h5 id="第2次握手传回了ACK，为什么还要传回SYN？"><a href="#第2次握手传回了ACK，为什么还要传回SYN？" class="headerlink" title="第2次握手传回了ACK，为什么还要传回SYN？"></a>第2次握手传回了ACK，为什么还要传回SYN？</h5><p>服务端传回发送端所发送的 ACK 是为了告诉客户端：“我接收到的信息确实就是你所发送的信号了”，这表明从客户端到服务端的通信是正常的。回传 SYN 则是为了建立并确认从服务端到客户端的通信。</p>
<blockquote>
<p>SYN 同步序列编号(Synchronize Sequence Numbers) 是 TCP&#x2F;IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时，客户机首先发出一个 SYN 消息，服务器使用 SYN-ACK 应答表示接收到了这个消息，最后客户机再以 ACK(Acknowledgement）消息响应。这样在客户机和服务器之间才能建立起可靠的 TCP 连接，数据才可以在客户机和服务器之间传递。</p>
</blockquote>
<hr>
<h4 id="x3D-x3D-断开连接-TCP-四次挥手-x3D-x3D"><a href="#x3D-x3D-断开连接-TCP-四次挥手-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;断开连接-TCP 四次挥手&#x3D;&#x3D;"></a>&#x3D;&#x3D;断开连接-TCP 四次挥手&#x3D;&#x3D;</h4><p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/cs-basics/network/tcp-waves-four-times.png" alt="TCP 四次挥手图解"></p>
<p>断开一个 TCP 连接则需要“四次挥手”，缺一不可 ：</p>
<ol>
<li><strong>第一次挥手</strong> ：客户端发送一个 FIN（SEQ&#x3D;X） 标志的数据包-&gt;服务端，用来关闭客户端到服务器的数据传送。然后，客户端进入 <strong>FIN-WAIT-1</strong> 状态。</li>
<li><strong>第二次挥手</strong> ：服务器收到这个 FIN（SEQ&#x3D;X） 标志的数据包，它发送一个 ACK （SEQ&#x3D;X+1）标志的数据包-&gt;客户端 。然后，此时服务端进入<strong>CLOSE-WAIT</strong>状态，客户端进入<strong>FIN-WAIT-2</strong>状态。</li>
<li><strong>第三次挥手</strong> ：服务端关闭与客户端的连接并发送一个 FIN (SEQ&#x3D;y)标志的数据包-&gt;客户端请求关闭连接，然后，服务端进入<strong>LAST-ACK</strong>状态。</li>
<li><strong>第四次挥手</strong> ：客户端发送 ACK (SEQ&#x3D;y+1)标志的数据包-&gt;服务端并且进入<strong>TIME-WAIT</strong>状态，服务端在收到 ACK (SEQ&#x3D;y+1)标志的数据包后进入 CLOSE 状态。此时，&#x3D;&#x3D;如果客户端等待 <strong>2MSL</strong> 后依然没有收到回复，就证明服务端已正常关闭，随后，客户端也可以关闭连接了。&#x3D;&#x3D;</li>
</ol>
<p><strong>只要四次挥手没有结束，客户端和服务端就可以继续传输数据！</strong></p>
<hr>
<h5 id="为什么要四次挥手？"><a href="#为什么要四次挥手？" class="headerlink" title="为什么要四次挥手？"></a>为什么要四次挥手？</h5><p>TCP是全双工通信，可以双向传输数据。任何一方都可以在数据传送结束后发出连接释放的通知，待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候，则发出连接释放通知，对方确认后就完全关闭了 TCP 连接。</p>
<p>举个例子：A 和 B 打电话，通话即将结束后。</p>
<ol>
<li><strong>第一次挥手</strong> ： A 说“我没啥要说的了”</li>
<li><strong>第二次挥手</strong> ：B 回答“我知道了”，但是 B 可能还会有要说的话，A 不能要求 B 跟着自己的节奏结束通话</li>
<li><strong>第三次挥手</strong> ：于是 B 可能又巴拉巴拉说了一通，最后 B 说“我说完了”</li>
<li><strong>第四次挥手</strong> ：A 回答“知道了”，这样通话才算结束。</li>
</ol>
<hr>
<h5 id="x3D-x3D-为什么不能把服务器发送的-ACK-和-FIN-合并起来，变成三次挥手？-x3D-x3D"><a href="#x3D-x3D-为什么不能把服务器发送的-ACK-和-FIN-合并起来，变成三次挥手？-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;为什么不能把服务器发送的 ACK 和 FIN 合并起来，变成三次挥手？&#x3D;&#x3D;"></a>&#x3D;&#x3D;为什么不能把服务器发送的 ACK 和 FIN 合并起来，变成三次挥手？&#x3D;&#x3D;</h5><p>因为服务器收到客户端断开连接的请求时，可能还有一些数据没有发完，这时先回复 ACK，表示接收到了断开连接的请求。等到数据发完之后再发 FIN，断开服务器到客户端的数据传送。</p>
<h5 id="如果第二次挥手时服务器的-ACK-没有送达客户端，会怎样？"><a href="#如果第二次挥手时服务器的-ACK-没有送达客户端，会怎样？" class="headerlink" title="如果第二次挥手时服务器的 ACK 没有送达客户端，会怎样？"></a>如果第二次挥手时服务器的 ACK 没有送达客户端，会怎样？</h5><p>客户端没有收到 ACK 确认，会重新发送 FIN 请求。</p>
<h5 id="x3D-x3D-为什么第四次挥手客户端需要等待-2-MSL（报文段最长寿命）时间后才进入-CLOSED-状态？-x3D-x3D"><a href="#x3D-x3D-为什么第四次挥手客户端需要等待-2-MSL（报文段最长寿命）时间后才进入-CLOSED-状态？-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;为什么第四次挥手客户端需要等待 2*MSL（报文段最长寿命）时间后才进入 CLOSED 状态？&#x3D;&#x3D;"></a>&#x3D;&#x3D;为什么第四次挥手客户端需要等待 2*MSL（报文段最长寿命）时间后才进入 CLOSED 状态？&#x3D;&#x3D;</h5><p>因为服务器收到客户端断开连接的请求时，可能还有一些数据没有发完，这时先回复 ACK，表示接收到了断开连接的请求。等到数据发完之后再发 FIN，断开服务器到客户端的数据传送。（当Client端的ACK发送失败时，Server重新发送FIN，直到Server收到ACK）2*MSL指的是Client发送ACK且没有收到FIN。</p>
<blockquote>
<p><strong>MSL(Maximum Segment Lifetime)</strong> : 一个片段在网络中最大的存活时间，2MSL 就是一个发送和一个回复所需的最大时间。如果直到 2MSL，Client 都没有再次收到 FIN，那么 Client 推断 ACK 已经被成功接收，则结束 TCP 连接。</p>
</blockquote>
<hr>
<h3 id="TCP-传输可靠性保障（传输层）"><a href="#TCP-传输可靠性保障（传输层）" class="headerlink" title="TCP 传输可靠性保障（传输层）"></a>TCP 传输可靠性保障（传输层）</h3><h4 id="x3D-x3D-TCP-如何保证传输的可靠性？（重要）-x3D-x3D"><a href="#x3D-x3D-TCP-如何保证传输的可靠性？（重要）-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;TCP 如何保证传输的可靠性？（重要）&#x3D;&#x3D;"></a>&#x3D;&#x3D;TCP 如何保证传输的可靠性？（重要）&#x3D;&#x3D;</h4><ol>
<li><strong>基于数据块传输</strong> ：应用数据被分割成 TCP 认为最适合发送的数据块，再传输给网络层，数据块被称为报文段或段。</li>
<li><strong>对失序数据包重新排序以及去重</strong>：TCP 为了保证不发生丢包，就给每个包一个序列号，有了序列号能够将接收到的数据根据序列号排序，并且去掉重复序列号的数据就可以实现数据包去重。</li>
<li><strong>校验和</strong> : TCP 将保持它首部和数据的检验和。这是一个端到端的检验和，目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错，TCP 将丢弃这个报文段和不确认收到此报文段。</li>
<li><strong>超时重传</strong> : 当发送方发送数据之后，它启动一个定时器，等待目的端确认收到这个报文段。接收端实体对已成功收到的包发回一个相应的确认信息（ACK）。如果发送端实体在合理的往返时延（&#x3D;&#x3D;RTT&#x3D;&#x3D;）内未收到确认消息，那么对应的数据包就被假设为<a target="_blank" rel="noopener" href="https://zh.wikipedia.org/wiki/%E4%B8%A2%E5%8C%85">已丢失open in new window</a>并进行重传。</li>
<li><strong>流量控制</strong> : TCP 连接的每一方都有固定大小的缓冲空间，TCP 的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据，能提示发送方降低发送的速率，防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议（TCP 利用滑动窗口实现流量控制）。</li>
<li><strong>拥塞控制</strong> : 当网络拥塞时，减少数据的发送。</li>
</ol>
<hr>
<h4 id="TCP-如何实现流量控制？"><a href="#TCP-如何实现流量控制？" class="headerlink" title="TCP 如何实现流量控制？"></a>TCP 如何实现流量控制？</h4><p><strong>TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率，保证接收方来得及接收。</strong> 接收方发送的确认报文中的窗口字段可以用来&#x3D;&#x3D;控制发送方窗口大小&#x3D;&#x3D;，从而影响发送方的发送速率。将窗口字段设置为 0，则发送方不能发送数据。</p>
<p><strong>为什么需要流量控制?</strong> 这是因为双方在通信的时候，发送方的速率与接收方的速率是不一定相等，如果发送方的发送速率太快，会导致接收方处理不过来。如果接收方处理不过来的话，就只能把处理不过来的数据存在 <strong>接收缓冲区(Receiving Buffers)</strong> 里（失序的数据包也会被存放在缓存区里）。如果缓存区满了发送方还在狂发数据的话，接收方只能把收到的数据包丢掉。出现丢包问题的同时又疯狂浪费着珍贵的网络资源。因此，我们需要控制发送方的发送速率，让接收方与发送方处于一种动态平衡才好。</p>
<p>TCP 为<strong>全双工</strong>(Full-Duplex, FDX)通信，双方可以进行双向通信，<strong>客户端和服务端既可能是发送端又可能是接收端</strong>。因此，两端各有一个发送缓冲区与接收缓冲区，两端都各自维护一个发送窗口和一个接收窗口。接收窗口大小取决于应用、系统、硬件的限制（TCP传输速率不能大于应用的数据处理速率）。通信双方的发送窗口和接收窗口的要求相同</p>
<p><strong>TCP 发送窗口可以划分成四个部分</strong> ：</p>
<ol>
<li>已经发送并且确认的TCP段（已经发送并确认）；</li>
<li>已经发送但是没有确认的TCP段（已经发送未确认）；</li>
<li>未发送但是接收方准备接收的TCP段（可以发送）；</li>
<li>未发送并且接收方也并未准备接受的TCP段（不可发送）。</li>
</ol>
<p><strong>TCP发送窗口结构图示</strong> ：</p>
<p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/cs-basics/network/tcp-send-window.png" alt="TCP发送窗口结构"></p>
<ul>
<li><strong>SND.WND</strong> ：发送窗口。</li>
<li><strong>SND.UNA</strong>：Send Unacknowledged 指针，指向发送窗口的第一个字节。</li>
<li><strong>SND.NXT</strong>：Send Next 指针，指向可用窗口的第一个字节。</li>
</ul>
<p><strong>可用窗口大小</strong> &#x3D; <code>SND.UNA + SND.WND - SND.NXT</code> 。</p>
<p><strong>TCP 接收窗口可以划分成三个部分</strong> ：</p>
<ol>
<li>已经接收并且已经确认的 TCP 段（已经接收并确认）；</li>
<li>等待接收且允许发送方发送 TCP 段（可以接收未确认）；</li>
<li>不可接收且不允许发送方发送TCP段（不可接收）</li>
</ol>
<p><strong>TCP 接收窗口结构图示</strong> ：</p>
<p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/cs-basics/network/tcp-receive-window.png" alt="TCP接收窗口结构">	<strong>接收窗口的大小是根据接收端处理数据的速度动态调整的。</strong> 如果接收端读取数据快，接收窗口可能会扩大。 否则，它可能会缩小。</p>
<p>另外，这里的滑动窗口大小只是为了演示使用，实际窗口大小通常会远远大于这个值。</p>
<hr>
<h4 id="TCP-的拥塞控制是怎么实现的？"><a href="#TCP-的拥塞控制是怎么实现的？" class="headerlink" title="TCP 的拥塞控制是怎么实现的？"></a>TCP 的拥塞控制是怎么实现的？</h4><p>拥塞控制是一个全局性的过程，涉及到所有的主机，所有的路由器，以及与降低网络传输性能有关的所有因素。相反，流量控制往往是点对点通信量的控制，是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率，以便使接收端来得及接收。</p>
<p>TCP 的拥塞控制采用了四种算法，即 <strong>慢开始</strong> 、 <strong>拥塞避免</strong> 、<strong>快重传</strong> 和 <strong>快恢复</strong>。 </p>
<p><strong>慢开始：</strong> 慢开始算法的思路是当主机开始发送数据时，如果立即把大量数据字节注入到网络，那么可能会引起网络阻塞，因为现在还不知道网络的符合情况。经验表明，较好的方法是先探测一下，即由小到大逐渐增大发送窗口，也就是由小到大逐渐增大拥塞窗口数值。cwnd 初始值为 1，每经过一个传播轮次，cwnd 加倍。</p>
<p>慢启动具有阈值：</p>
<p><strong>拥塞避免：</strong> 拥塞避免算法的思路是让拥塞窗口 cwnd 缓慢增大，即每经过一个往返时间 RTT 就把发送方的 cwnd 加 1.</p>
<p><strong>快重传与快恢复：</strong> 在 TCP&#x2F;IP 中，快速重传和恢复（fast retransmit and recovery，FRR）是一种拥塞控制算法，它能快速恢复丢失的数据包。没有 FRR，如果数据包丢失了，TCP 将会使用定时器来要求传输暂停。在暂停的这段时间内，没有新的或复制的数据包被发送。有了 FRR，如果接收机接收到一个不按顺序的数据段，它会立即给发送机发送一个重复确认。如果发送机接收到三个重复确认，它会假定确认件指出的数据段丢失了，并立即重传这些丢失的数据段。有了 FRR，就不会因为重传时要求的暂停被耽误。 　当有单独的数据包丢失时，快速重传和恢复（FRR）能最有效地工作。当有多个数据信息包在某一段很短的时间内丢失时，它则不能很有效地工作。</p>
<p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/cs-basics/network/tcp-congestion-control.png" alt="TCP的拥塞控制"></p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230327185259706.png" alt="image-20230327185259706"></p>
<p><img src="https://s2.loli.net/2023/03/27/62nxJ4HAupo3Xl1.png"></p>
<p><strong>快重传后执行快恢复算法</strong></p>
<hr>
<h4 id="x3D-x3D-ARQ-协议了解吗-x3D-x3D"><a href="#x3D-x3D-ARQ-协议了解吗-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;ARQ 协议了解吗?&#x3D;&#x3D;"></a>&#x3D;&#x3D;ARQ 协议了解吗?&#x3D;&#x3D;</h4><p><strong>自动重传请求</strong>（Automatic Repeat-reQuest，ARQ）是 OSI 模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认和超时这两个机制，在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送后一段时间之内没有收到确认信息（Acknowledgements，就是我们常说的 ACK），它通常会重新发送，直到收到确认或者重试超过一定的次数。</p>
<p>ARQ 包括停止等待 ARQ 协议和连续 ARQ 协议。</p>
<h5 id="停止等待-ARQ-协议"><a href="#停止等待-ARQ-协议" class="headerlink" title="停止等待 ARQ 协议"></a>停止等待 ARQ 协议</h5><p> 停止等待协议是为了实现可靠传输的，它的基本原理就是每发完一个分组就停止发送，等待对方确认（回复 ACK）。如果过了一段时间（超时时间后），还是没有收到 ACK 确认，说明没有发送成功，需要重新发送，直到收到确认后再发下一个分组；</p>
<p>在停止等待协议中，若接收方收到重复分组，就丢弃该分组，但同时还要发送确认。</p>
<p><strong>1) 无差错情况:</strong></p>
<p>发送方发送分组,接收方在规定时间内收到,并且回复确认.发送方再次发送。</p>
<p><strong>2) 出现差错情况（超时重传）:</strong></p>
<p>停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认，就重传前面发送过的分组（认为刚才发送过的分组丢失了）。因此每发送完一个分组需要设置一个超时计时器，其重传时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为 <strong>自动重传请求 ARQ</strong> 。另外在停止等待协议中若收到重复分组，就丢弃该分组，但同时还要发送确认。</p>
<p><strong>3) 确认丢失和确认迟到</strong></p>
<p> <strong>确认丢失</strong> ：确认消息在传输过程丢失。当 A 发送 M1 消息，B 收到后，B 向 A 发送了一个 M1 确认消息，但却在传输过程中丢失。而 A 并不知道，在超时计时过后，A 重传 M1 消息，B 再次收到该消息后采取以下两点措施：1. 丢弃这个重复的 M1 消息，不向上层交付。 2. 向 A 发送确认消息。（不会认为已经发送过了，就不再发送。A 能重传，就证明 B 的确认消息丢失）。</p>
<p><strong>确认迟到</strong> ：确认消息在传输过程中迟到。A 发送 M1 消息，B 收到并发送确认。在超时时间内A没有收到确认消息，A 重传 M1 消息，B 仍然收到并继续发送确认消息（B 收到了 2 份 M1）。此时 A 收到了 B 第二次发送的确认消息。接着发送其他数据。过了一会，A 收到了 B 第一次发送的对 M1 的确认消息（A 也收到了 2 份确认消息）。处理如下：1. A 收到重复的确认后，直接丢弃。2. B 收到重复的 M1 后，也直接丢弃重复的 M1。</p>
<hr>
<h5 id="连续-ARQ-协议"><a href="#连续-ARQ-协议" class="headerlink" title="连续 ARQ 协议"></a>连续 ARQ 协议</h5><p>连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口，凡位于发送窗口内的分组可以连续发送出去，而不需要等待对方确认。接收方一般采用累计确认，&#x3D;&#x3D;对按序到达的最后一个分组发送确认&#x3D;&#x3D;，表明到这个分组为止的所有分组都已经正确收到了。</p>
<p><strong>优点：</strong> 信道利用率高，容易实现，即使确认丢失，也不必重传。</p>
<p><strong>缺点：</strong> 不能向发送方反映出接收方已经正确收到的所有分组的信息。 比如：发送方发送了 5 条 消息，中间第三条丢失（3 号），这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落，而只好把后三个全部重传一次。这也叫 Go-Back-N（回退 N），表示需要退回来重传已经发送过的 N 个消息。</p>
<hr>
<h2 id="HTTP"><a href="#HTTP" class="headerlink" title="HTTP"></a>HTTP</h2><h3 id="从输入URL-到页面展示到底发生了什么？（非常重要）"><a href="#从输入URL-到页面展示到底发生了什么？（非常重要）" class="headerlink" title="从输入URL 到页面展示到底发生了什么？（非常重要）"></a>从输入URL 到页面展示到底发生了什么？（非常重要）</h3><blockquote>
<p>类似的问题：打开一个网页，整个过程会使用哪些协议？</p>
</blockquote>
<p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/url%E8%BE%93%E5%85%A5%E5%88%B0%E5%B1%95%E7%A4%BA%E5%87%BA%E6%9D%A5%E7%9A%84%E8%BF%87%E7%A8%8B.jpg" alt="img"></p>
<blockquote>
<p>上图有一个错误，请注意，是 OSPF 不是 OPSF。 OSPF（Open Shortest Path First，ospf）开放最短路径优先协议, 是由 Internet 工程任务组开发的路由选择协议</p>
</blockquote>
<h3 id="HTTP-状态码有哪些？"><a href="#HTTP-状态码有哪些？" class="headerlink" title="HTTP 状态码有哪些？"></a>HTTP 状态码有哪些？</h3><p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/%E7%8A%B6%E6%80%81%E7%A0%81.png" alt="HTTP 状态码"></p>
<h4 id="1xx-Informational（信息性状态码）"><a href="#1xx-Informational（信息性状态码）" class="headerlink" title="1xx Informational（信息性状态码）"></a>1xx Informational（信息性状态码）</h4><h4 id="2xx-Success（成功状态码）"><a href="#2xx-Success（成功状态码）" class="headerlink" title="2xx Success（成功状态码）"></a>2xx Success（成功状态码）</h4><ul>
<li><strong>200 OK</strong> ：请求被成功处理。比如我们发送一个查询用户数据的HTTP 请求到服务端，服务端正确返回了用户数据。这个是我们平时最常见的一个 HTTP 状态码。</li>
<li><strong>201 Created</strong> ：请求被成功处理并且在服务端创建了一个新的资源。比如我们通过 POST 请求创建一个新的用户。</li>
<li><strong>202 Accepted</strong> ：服务端已经接收到了请求，但是还未处理。</li>
<li><strong>204 No Content</strong> ： 服务端已经成功处理了请求，但是没有返回任何内容。</li>
</ul>
<h4 id="3xx-Redirection（重定向状态码）"><a href="#3xx-Redirection（重定向状态码）" class="headerlink" title="3xx Redirection（重定向状态码）"></a>3xx Redirection（重定向状态码）</h4><ul>
<li><strong>301 Moved Permanently</strong> ： 资源被永久重定向了。比如你的网站的网址更换了。</li>
<li><strong>302 Found</strong> ：资源被临时重定向了。比如你的网站的某些资源被暂时转移到另外一个网址。</li>
</ul>
<h4 id="4xx-Client-Error（客户端错误状态码）"><a href="#4xx-Client-Error（客户端错误状态码）" class="headerlink" title="4xx Client Error（客户端错误状态码）"></a>4xx Client Error（客户端错误状态码）</h4><ul>
<li><strong>400 Bad Request</strong> ： 发送的HTTP请求存在问题。比如请求参数不合法、请求方法错误。</li>
<li><strong>401 Unauthorized</strong> ： 未认证请求需要认证之后才能访问的资源。</li>
<li><strong>403 Forbidden</strong> ：直接拒绝HTTP请求，不处理。一般用来针对非法请求。</li>
<li><strong>404 Not Found</strong> ： 你请求的资源未在服务端找到。比如你请求某个用户的信息，服务端并没有找到指定的用户。</li>
<li><strong>409 Conflict</strong> ： 表示请求的资源与服务端当前的状态存在冲突，请求无法被处理。</li>
</ul>
<h4 id="5xx-Server-Error（服务端错误状态码）"><a href="#5xx-Server-Error（服务端错误状态码）" class="headerlink" title="5xx Server Error（服务端错误状态码）"></a>5xx Server Error（服务端错误状态码）</h4><ul>
<li><strong>500 Internal Server Error</strong> ： 服务端出问题了（通常是服务端出Bug了）。比如你服务端处理请求的时候突然抛出异常，但是异常并未在服务端被正确处理。</li>
<li><strong>502 Bad Gateway</strong> ：我们的网关将请求转发到服务端，但是服务端返回的却是一个错误的响应。</li>
</ul>
<hr>
<h3 id="x3D-x3D-HTTP-和-HTTPS-有什么区别？（重要）-x3D-x3D"><a href="#x3D-x3D-HTTP-和-HTTPS-有什么区别？（重要）-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;HTTP 和 HTTPS 有什么区别？（重要）&#x3D;&#x3D;"></a>&#x3D;&#x3D;HTTP 和 HTTPS 有什么区别？（重要）&#x3D;&#x3D;</h3><ul>
<li><strong>端口号</strong> ：HTTP 默认是 80，HTTPS 默认是 443。</li>
<li><strong>URL 前缀</strong> ：HTTP 的 URL 前缀是 <code>http://</code>，HTTPS 的 URL 前缀是 <code>https://</code>。</li>
<li><strong>安全性和资源消耗</strong> ： HTTP 协议运行在 TCP 之上，所有传输的内容都是明文，客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL&#x2F;TLS 之上的 HTTP 协议，SSL&#x2F;TLS（Secure Socket Layer&#x2F;Transport Layer Security） 运行在 TCP 之上。所有传输的内容都经过加密，加密采用对称加密，但对称加密的密钥用服务器方的证书进行了非对称加密。所以说，HTTP 安全性没有 HTTPS 高，但是 HTTPS 比 HTTP 耗费更多服务器资源。</li>
</ul>
<h4 id="HTTP-协议"><a href="#HTTP-协议" class="headerlink" title="HTTP 协议"></a>HTTP 协议</h4><h5 id="HTTP-协议通信过程"><a href="#HTTP-协议通信过程" class="headerlink" title="HTTP 协议通信过程"></a>HTTP 协议通信过程</h5><p>HTTP 是应用层协议，它以 TCP（传输层）作为底层协议，默认端口为 80. 通信过程主要如下：</p>
<ol>
<li>服务器在 80 端口等待客户的请求。</li>
<li>浏览器发起到服务器的 TCP 连接（创建套接字 Socket）。</li>
<li>服务器接收来自浏览器的 TCP 连接。</li>
<li>浏览器（HTTP 客户端）与 Web 服务器（HTTP 服务器）交换 HTTP 消息。</li>
<li>关闭 TCP 连接。</li>
</ol>
<hr>
<h5 id="HTTP-协议优点"><a href="#HTTP-协议优点" class="headerlink" title="HTTP 协议优点"></a>HTTP 协议优点</h5><p>扩展性强、速度快、跨平台支持性好。</p>
<h4 id="HTTPS-协议"><a href="#HTTPS-协议" class="headerlink" title="HTTPS 协议"></a>HTTPS 协议</h4><h5 id="HTTPS-协议介绍"><a href="#HTTPS-协议介绍" class="headerlink" title="HTTPS 协议介绍"></a>HTTPS 协议介绍</h5><p>HTTPS 协议（Hyper Text Transfer Protocol Secure），是 HTTP 的加强安全版本。HTTPS 是基于 HTTP 的，也是用 TCP 作为底层协议，并额外使用 SSL&#x2F;TLS 协议用作加密和安全认证。默认端口号是 443.</p>
<p>HTTPS 协议中，SSL 通道通常使用基于密钥的加密算法，密钥长度通常是 40 比特或 128 比特。</p>
<hr>
<h5 id="HTTPS-协议优点"><a href="#HTTPS-协议优点" class="headerlink" title="HTTPS 协议优点"></a>HTTPS 协议优点</h5><p>保密性好、信任度高。</p>
<hr>
<h4 id="HTTPS-的核心—SSL-x2F-TLS协议"><a href="#HTTPS-的核心—SSL-x2F-TLS协议" class="headerlink" title="HTTPS 的核心—SSL&#x2F;TLS协议"></a>HTTPS 的核心—SSL&#x2F;TLS协议</h4><p>HTTPS 之所以能达到较高的安全性要求，就是结合了 SSL&#x2F;TLS 和 TCP 协议，对通信数据进行加密，解决了 HTTP 数据透明的问题。接下来重点介绍一下 SSL&#x2F;TLS 的工作原理。</p>
<h5 id="SSL-和-TLS-的区别？"><a href="#SSL-和-TLS-的区别？" class="headerlink" title="SSL 和 TLS 的区别？"></a>SSL 和 TLS 的区别？</h5><p><strong>SSL 和 TLS 没有太大的区别。</strong></p>
<p>SSL 指安全套接字协议（Secure Sockets Layer），首次发布与 1996 年。SSL 的首次发布其实已经是他的 3.0 版本，SSL 1.0 从未面世，SSL 2.0 则具有较大的缺陷（DROWN 缺陷——Decrypting RSA with Obsolete and Weakened eNcryption）。很快，在 1999 年，SSL 3.0 进一步升级，<strong>新版本被命名为 TLS 1.0</strong>。因此，TLS 是基于 SSL 之上的，但由于习惯叫法，通常把 HTTPS 中的核心加密协议混称为 SSL&#x2F;TLS。</p>
<hr>
<h5 id="SSL-x2F-TLS-的工作原理"><a href="#SSL-x2F-TLS-的工作原理" class="headerlink" title="SSL&#x2F;TLS 的工作原理"></a>SSL&#x2F;TLS 的工作原理</h5><h6 id="非对称加密"><a href="#非对称加密" class="headerlink" title="非对称加密"></a>非对称加密</h6><p>SSL&#x2F;TLS 的核心要素是<strong>非对称加密</strong>。非对称加密采用两个密钥——一个公钥，一个私钥。在通信时，私钥仅由解密者保存，公钥由任何一个想与解密者通信的发送者（加密者）所知。可以设想一个场景</p>
<blockquote>
<p>在某个自助邮局，每个通信信道都是一个邮箱，每一个邮箱所有者都在旁边立了一个牌子，上面挂着一把钥匙：这是我的公钥，发送者请将信件放入我的邮箱，并用公钥锁好。</p>
<p>但是公钥只能加锁，并不能解锁。解锁只能由邮箱的所有者——因为只有他保存着私钥。</p>
<p>这样，通信信息就不会被其他人截获了，这依赖于私钥的保密性。</p>
</blockquote>
<blockquote>
<p>单向函数：已知单向函数 f，给定任意一个输入 x，易计算输出 y&#x3D;f(x)；而给定一个输出 y，假设存在 f(x)&#x3D;y，很难根据 f 来计算出 x。</p>
<p>单向陷门函数：一个较弱的单向函数。已知单向陷门函数 f，陷门 h，给定任意一个输入 x，易计算出输出 y&#x3D;f(x;h)；而给定一个输出 y，假设存在 f(x;h)&#x3D;y，很难根据 f 来计算出 x，但可以根据 f 和 h 来推导出 x。</p>
</blockquote>
<hr>
<h6 id="对称加密"><a href="#对称加密" class="headerlink" title="对称加密"></a>对称加密</h6><p>使用 SSL&#x2F;TLS 进行通信的双方需要使用非对称加密方案来通信，但是非对称加密设计了较为复杂的数学算法，在实际通信过程中，计算的代价较高，效率太低，因此，SSL&#x2F;TLS 实际对消息的加密使用的是对称加密。</p>
<blockquote>
<p>对称加密：通信双方共享唯一密钥 k，加解密算法已知，加密方利用密钥 k 加密，解密方利用密钥 k 解密，保密性依赖于密钥 k 的保密性。</p>
</blockquote>
<hr>
<p>对称加密的密钥生成代价比公私钥对的生成代价低得多，那么有的人会问了，为什么 SSL&#x2F;TLS 还需要使用非对称加密呢？因为对称加密的保密性完全依赖于密钥的保密性。在双方通信之前，需要商量一个用于对称加密的密钥。我们知道网络通信的信道是不安全的，传输报文对任何人是可见的，密钥的交换肯定不能直接在网络信道中传输。因此，使用非对称加密，对对称加密的密钥进行加密，保护该密钥不在网络信道中被窃听。这样，通信双方只需要一次非对称加密，交换对称加密的密钥，在之后的信息通信中，使用绝对安全的密钥，对信息进行对称加密，即可保证传输消息的保密性。</p>
<hr>
<h6 id="公钥传输的信赖性"><a href="#公钥传输的信赖性" class="headerlink" title="公钥传输的信赖性"></a>公钥传输的信赖性</h6><blockquote>
<p>客户端 C 和服务器 S 想要使用 SSL&#x2F;TLS 通信，由上述 SSL&#x2F;TLS 通信原理，C 需要先知道 S 的公钥，而 S 公钥的唯一获取途径，就是把 S 公钥在网络信道中传输。要注意网络信道通信中有几个前提：</p>
<ol>
<li>任何人都可以捕获通信包</li>
<li>通信包的保密性由发送者设计</li>
<li>保密算法设计方案默认为公开，而（解密）密钥默认是安全的</li>
</ol>
<p>因此，假设 S 公钥不做加密，在信道中传输，那么很有可能存在一个攻击者 A，发送给 C 一个诈包，假装是 S 公钥，其实是诱饵服务器 AS 的公钥。当 C 收获了 AS 的公钥（却以为是 S 的公钥），C 后续就会使用 AS 公钥对数据进行加密，并在公开信道传输，那么 A 将捕获这些加密包，用 AS 的私钥解密，就截获了 C 本要给 S 发送的内容，而 C 和 S 二人全然不知。</p>
<p>同样的，S 公钥即使做加密，也难以避免这种信任性问题，C 被 AS 拐跑了！</p>
</blockquote>
<p>为了公钥传输的信赖性问题，第三方机构应运而生——证书颁发机构（CA，Certificate Authority）。CA 默认是受信任的第三方。CA 会给各个服务器颁发证书，证书存储在服务器上，并附有 CA 的<strong>电子签名</strong>（见下节）。</p>
<p>当客户端（浏览器）向服务器发送 HTTPS 请求时，一定要先获取目标服务器的证书，并根据证书上的信息，检验证书的合法性。一旦客户端检测到证书非法，就会发生错误。客户端获取了服务器的证书后，由于证书的信任性是由第三方信赖机构认证的，而证书上又包含着服务器的公钥信息，客户端就可以放心的信任证书上的公钥就是目标服务器的公钥</p>
<hr>
<h5 id="数字签名"><a href="#数字签名" class="headerlink" title="数字签名"></a>数字签名</h5><p>好，到这一小节，已经是 SSL&#x2F;TLS 的尾声了。上一小节提到了数字签名，数字签名要解决的问题，是防止证书被伪造。第三方信赖机构 CA 之所以能被信赖，就是 <strong>靠数字签名技术</strong> 。</p>
<p>总结来说，带有证书的公钥传输机制如下：</p>
<ol>
<li>设有服务器 S，客户端 C，和第三方信赖机构 CA。</li>
<li>S 信任 CA，CA 是知道 S 公钥的，CA 向 S 颁发证书。并附上 CA 私钥对消息摘要的加密签名。</li>
<li>S 获得 CA 颁发的证书，将该证书传递给 C。</li>
<li>C 获得 S 的证书，信任 CA 并知晓 CA 公钥，使用 CA 公钥对 S 证书上的签名解密，同时对消息进行散列处理，得到摘要。比较摘要，验证 S 证书的真实性。</li>
<li>如果 C 验证 S 证书是真实的，则信任 S 的公钥（在 S 证书中）。</li>
</ol>
<hr>
<h4 id="总结-3"><a href="#总结-3" class="headerlink" title="总结"></a>总结</h4><ul>
<li><strong>端口号</strong> ：HTTP 默认是 80，HTTPS 默认是 443。</li>
<li><strong>URL 前缀</strong> ：HTTP 的 URL 前缀是 <code>http://</code>，HTTPS 的 URL 前缀是 <code>https://</code>。</li>
<li><strong>安全性和资源消耗</strong> ： HTTP 协议运行在 TCP 之上，所有传输的内容都是明文，客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL&#x2F;TLS 之上的 HTTP 协议，SSL&#x2F;TLS 运行在 TCP 之上。所有传输的内容都经过加密，加密采用对称加密，但对称加密的密钥用服务器方的证书进行了非对称加密。所以说，HTTP 安全性没有 HTTPS 高，但是 HTTPS 比 HTTP 耗费更多服务器资源。</li>
</ul>
<h3 id="x3D-x3D-HTTP-1-0-和-HTTP-1-1-有什么区别？-x3D-x3D"><a href="#x3D-x3D-HTTP-1-0-和-HTTP-1-1-有什么区别？-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;HTTP 1.0 和 HTTP 1.1 有什么区别？&#x3D;&#x3D;"></a>&#x3D;&#x3D;HTTP 1.0 和 HTTP 1.1 有什么区别？&#x3D;&#x3D;</h3><ul>
<li><strong>连接方式</strong> : HTTP 1.0 为短连接，HTTP 1.1 支持长连接。</li>
<li><strong>状态响应码</strong> : HTTP&#x2F;1.1中新加入了大量的状态码，光是错误响应状态码就新增了24种。比如说，<code>100 (Continue)</code>——在请求大资源前的预热请求，<code>206 (Partial Content)</code>——范围请求的标识码，<code>409 (Conflict)</code>——请求与当前资源的规定冲突，<code>410 (Gone)</code>——资源已被永久转移，而且没有任何已知的转发地址。</li>
<li><strong>缓存处理</strong> : 在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准，HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag，If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓存策略。</li>
<li><strong>带宽优化及网络连接的使用</strong> :HTTP1.0 中，存在一些浪费带宽的现象，例如客户端只是需要某个对象的一部分，而服务器却将整个对象送过来了，并且不支持断点续传功能，HTTP1.1 则在请求头引入了 range 头域，它允许只请求资源的某个部分，即返回码是 206（Partial Content），这样就方便了开发者自由的选择以便于充分利用带宽和连接。</li>
<li><strong>Host头处理</strong> : HTTP&#x2F;1.1在请求头中加入了<code>Host</code>字段。</li>
</ul>
<h4 id="缓存处理"><a href="#缓存处理" class="headerlink" title="缓存处理"></a>缓存处理</h4><h5 id="HTTP-x2F-1-0"><a href="#HTTP-x2F-1-0" class="headerlink" title="HTTP&#x2F;1.0"></a>HTTP&#x2F;1.0</h5><p>HTTP&#x2F;1.0提供的缓存机制非常简单。服务器端使用<code>Expires</code>标签来标志（时间）一个响应体，在<code>Expires</code>标志时间内的请求，都会获得该响应体缓存。服务器端在初次返回给客户端的响应体中，有一个<code>Last-Modified</code>标签，该标签标记了被请求资源在服务器端的最后一次修改。在请求头中，使用<code>If-Modified-Since</code>标签，该标签标志一个时间，意为客户端向服务器进行问询：“该时间之后，我要请求的资源是否有被修改过？”通常情况下，请求头中的<code>If-Modified-Since</code>的值即为上一次获得该资源时，响应体中的<code>Last-Modified</code>的值。</p>
<p>如果服务器接收到了请求头，并判断<code>If-Modified-Since</code>时间后，资源确实没有修改过，则返回给客户端一个<code>304 not modified</code>响应头，表示”缓冲可用，你从浏览器里拿吧！”。</p>
<p>如果服务器判断<code>If-Modified-Since</code>时间后，资源被修改过，则返回给客户端一个<code>200 OK</code>的响应体，并附带全新的资源内容，表示”你要的我已经改过的，给你一份新的”。</p>
<p><img src="https://javaguide.cn/assets/HTTP1.0cache1.2f2b7eac.png" alt="HTTP1.0cache1"><img src="https://javaguide.cn/assets/HTTP1.0cache2.7430070e.png" alt="HTTP1.0cache2"></p>
<h5 id="HTTP-x2F-1-1"><a href="#HTTP-x2F-1-1" class="headerlink" title="HTTP&#x2F;1.1"></a>HTTP&#x2F;1.1</h5><p>HTTP&#x2F;1.1的缓存机制在HTTP&#x2F;1.0的基础上，大大增加了灵活性和扩展性。基本工作原理和HTTP&#x2F;1.0保持不变，而是增加了更多细致的特性。其中，请求头中最常见的特性就是<code>Cache-Control</code>，</p>
<hr>
<h4 id="连接方式"><a href="#连接方式" class="headerlink" title="连接方式"></a>连接方式</h4><p><strong>HTTP&#x2F;1.0 默认使用短连接</strong> ，也就是说，客户端和服务器每进行一次 HTTP 操作，就建立一次连接，任务结束就中断连接</p>
<p><strong>为了解决 HTTP&#x2F;1.0 存在的资源浪费的问题， HTTP&#x2F;1.1 优化为默认长连接模式 。</strong> 采用长连接模式的请求报文会通知服务端：“我向你请求连接，并且连接成功建立后，请不要关闭”。因此，该TCP连接将持续打开，为后续的客户端-服务端的数据交互服务。也就是说在使用长连接的情况下，当一个网页打开完成后，客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭，客户端再次访问这个服务器时，会继续使用这一条已经建立的连接。</p>
<p>如果 TCP 连接一直保持的话也是对资源的浪费，因此，一些服务器软件（如 Apache）还会支持超时时间的时间。在超时时间之内没有新的请求达到，TCP 连接才会被关闭。</p>
<p>有必要说明的是，HTTP&#x2F;1.0仍提供了长连接选项，即在请求头中加入<code>Connection: Keep-alive</code>。同样的，在HTTP&#x2F;1.1中，如果不希望使用长连接选项，也可以在请求头中加入<code>Connection: close</code>，这样会通知服务器端：“我不需要长连接，连接成功后即可关闭”。</p>
<p>&#x3D;&#x3D;<strong>HTTP 协议的长连接和短连接，实质上是 TCP 协议的长连接和短连接。</strong>&#x3D;&#x3D;</p>
<p><strong>实现长连接需要客户端和服务端都支持长连接</strong></p>
<hr>
<h4 id="Host头处理"><a href="#Host头处理" class="headerlink" title="Host头处理"></a>Host头处理</h4><p>域名系统（DNS）允许多个主机名绑定到同一个IP地址上，但是HTTP&#x2F;1.0并没有考虑这个问题，假设我们有一个资源URL是<a target="_blank" rel="noopener" href="http://example1.org/home.html%EF%BC%8CHTTP/1.0%E7%9A%84%E8%AF%B7%E6%B1%82%E6%8A%A5%E6%96%87%E4%B8%AD%EF%BC%8C%E5%B0%86%E4%BC%9A%E8%AF%B7%E6%B1%82%E7%9A%84%E6%98%AF%60GET">http://example1.org/home.html，HTTP/1.0的请求报文中，将会请求的是`GET</a> &#x2F;home.html HTTP&#x2F;1.0&#96;.也就是不会加入主机名。这样的报文送到服务器端，服务器是理解不了客户端想请求的真正网址。</p>
<p>因此，HTTP&#x2F;1.1在请求头中加入了<code>Host</code>字段。加入<code>Host</code>字段的报文头部将会是:</p>
<figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">GET /home.html HTTP/1.1</span><br><span class="line">Host: example1.org</span><br></pre></td></tr></table></figure>

<p>这样，服务器端就可以确定客户端想要请求的真正的网址了。</p>
<hr>
<h4 id="带宽优化"><a href="#带宽优化" class="headerlink" title="带宽优化"></a>带宽优化</h4><h5 id="范围请求"><a href="#范围请求" class="headerlink" title="范围请求"></a>范围请求</h5><p>HTTP&#x2F;1.1引入了范围请求（range request）机制，以避免带宽的浪费。当客户端想请求一个文件的一部分，或者需要继续下载一个已经下载了部分但被终止的文件，HTTP&#x2F;1.1可以在请求中加入<code>Range</code>头部，以请求（并只能请求字节型数据）数据的一部分。服务器端可以忽略<code>Range</code>头部，也可以返回若干<code>Range</code>响应。</p>
<hr>
<h5 id="状态码100"><a href="#状态码100" class="headerlink" title="状态码100"></a>状态码100</h5><p>HTTP&#x2F;1.1中新加入了状态码<code>100</code>。该状态码的使用场景为，存在某些较大的文件请求，服务器可能不愿意响应这种请求，此时状态码<code>100</code>可以作为指示请求是否会被正常响应，过程如下图：</p>
<p><img src="" alt="HTTP1.1continue1"></p>
<p><img src="/xxd-blog/assets/HTTP1.1continue2.7d63532b.png" alt="HTTP1.1continue2"></p>
<p>然而在HTTP&#x2F;1.0中，并没有<code>100 (Continue)</code>状态码，要想触发这一机制，可以发送一个<code>Expect</code>头部，其中包含一个<code>100-continue</code>的值。</p>
<hr>
<h5 id="压缩"><a href="#压缩" class="headerlink" title="压缩"></a>压缩</h5><p>许多格式的数据在传输时都会做预压缩处理。数据的压缩可以大幅优化带宽的利用。然而，HTTP&#x2F;1.0对数据压缩的选项提供的不多，不支持压缩细节的选择，也无法区分端到端（end-to-end）压缩或者是逐跳（hop-by-hop）压缩。</p>
<p>HTTP&#x2F;1.1则对内容编码（content-codings）和传输编码（transfer-codings）做了区分。内容编码总是端到端的，传输编码总是逐跳的。</p>
<p>HTTP&#x2F;1.0包含了<code>Content-Encoding</code>头部，对消息进行端到端编码。HTTP&#x2F;1.1加入了<code>Transfer-Encoding</code>头部，可以对消息进行逐跳传输编码。HTTP&#x2F;1.1还加入了<code>Accept-Encoding</code>头部，是客户端用来指示他能处理什么样的内容编码。</p>
<hr>
<h4 id="总结-4"><a href="#总结-4" class="headerlink" title="总结"></a>总结</h4><ol>
<li><strong>连接方式</strong> : HTTP 1.0 为短连接，HTTP 1.1 支持长连接。</li>
<li><strong>状态响应码</strong> : HTTP&#x2F;1.1中新加入了大量的状态码，光是错误响应状态码就新增了24种。比如说，<code>100 (Continue)</code>——在请求大资源前的预热请求，<code>206 (Partial Content)</code>——范围请求的标识码，<code>409 (Conflict)</code>——请求与当前资源的规定冲突，<code>410 (Gone)</code>——资源已被永久转移，而且没有任何已知的转发地址。</li>
<li><strong>缓存处理</strong> : 在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准，HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag，If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓存策略。</li>
<li><strong>带宽优化及网络连接的使用</strong> :HTTP1.0 中，存在一些浪费带宽的现象，例如客户端只是需要某个对象的一部分，而服务器却将整个对象送过来了，并且不支持断&#x3D;&#x3D;点续传功能(利用Range（客户端）和Content-Range（服务器）实现)&#x3D;&#x3D;，HTTP1.1 则在请求头引入了 range 头域，它允许只请求资源的某个部分，即返回码是 206（Partial Content），这样就方便了开发者自由的选择以便于充分利用带宽和连接。</li>
<li><strong>Host头处理</strong> : HTTP&#x2F;1.1在请求头中加入了<code>Host</code>字段。</li>
</ol>
<hr>
<h3 id="HTTP-是不保存状态的协议-如何保存用户状态"><a href="#HTTP-是不保存状态的协议-如何保存用户状态" class="headerlink" title="HTTP 是不保存状态的协议, 如何保存用户状态?"></a>HTTP 是不保存状态的协议, 如何保存用户状态?</h3><p>HTTP 是一种不保存状态，即无状态（stateless）协议。也就是说 HTTP 协议自身不对请求和响应之间的通信状态进行保存。那么我们保存用户状态呢？Session 机制的存在就是为了解决这个问题，Session 的主要作用就是通过服务端记录用户的状态。典型的场景是购物车，当你要添加商品到购物车的时候，系统不知道是哪个用户操作的，因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了（一般情况下，服务器会在一定时间内保存这个 Session，过了时间限制，就会销毁这个 Session）。</p>
<p>在服务端保存 Session 的方法很多，最常用的就是内存和数据库(比如是使用内存数据库 redis 保存)。既然 Session 存放在服务器端，那么我们如何实现 Session 跟踪呢？大部分情况下，我们都是通过在 Cookie 中附加一个 Session ID 来方式来跟踪。</p>
<hr>
<p><strong>Cookie 被禁用怎么办?</strong></p>
<p>最常用的就是利用 URL 重写把 Session ID 直接附加在 URL 路径的后面。</p>
<h3 id="URI-和-URL-的区别是什么"><a href="#URI-和-URL-的区别是什么" class="headerlink" title="URI 和 URL 的区别是什么?"></a>URI 和 URL 的区别是什么?</h3><ul>
<li>URI(Uniform Resource Identifier) 是统一资源标志符，可以唯一标识一个资源。</li>
<li>URL(Uniform Resource Locator) 是统一资源定位符，可以提供该资源的路径。它是一种具体的 URI，即 URL 可以用来标识一个资源，而且还指明了如何 locate 这个资源。</li>
</ul>
<p>URI 的作用像身份证号一样，URL 的作用更像家庭住址一样。URL 是一种具体的 URI，它不仅唯一标识资源，而且还提供了定位该资源的信息。 </p>
<h2 id="ARP"><a href="#ARP" class="headerlink" title="ARP"></a>ARP</h2><h3 id="什么是-Mac-地址？"><a href="#什么是-Mac-地址？" class="headerlink" title="什么是 Mac 地址？"></a>什么是 Mac 地址？</h3><p>MAC 地址的全称是 <strong>媒体访问控制地址（Media Access Control Address）</strong>。如果说，互联网中每一个资源都由 IP 地址唯一标识（IP 协议内容），那么一切网络设备都由 MAC 地址唯一标识。</p>
<p>可以理解为，MAC 地址是一个网络设备真正的身份证号，IP 地址只是一种不重复的定位方式（比如说住在某省某市某街道的张三，这种逻辑定位是 IP 地址，他的身份证号才是他的 MAC 地址），也可以理解为 MAC 地址是身份证号，IP 地址是邮政地址。MAC 地址也有一些别称，如 LAN 地址、物理地址、以太网地址等。</p>
<hr>
<h3 id="ARP-协议解决了什么问题地位如何？"><a href="#ARP-协议解决了什么问题地位如何？" class="headerlink" title="ARP 协议解决了什么问题地位如何？"></a>ARP 协议解决了什么问题地位如何？</h3><p>ARP 协议，全称 <strong>地址解析协议（Address Resolution Protocol）</strong>，它解决的是网络层地址和链路层地址之间的转换问题。因为一个 IP 数据报在物理上传输的过程中，总是需要知道下一跳（物理上的下一个目的地）该去往何处，但 IP 地址属于逻辑地址，而 MAC 地址才是物理地址，ARP 协议解决了 IP 地址转 MAC 地址的一些问题。</p>
<hr>
<h3 id="ARP-协议的工作原理？"><a href="#ARP-协议的工作原理？" class="headerlink" title="ARP 协议的工作原理？"></a>ARP 协议的工作原理？</h3><p>ARP 协议工作时有一个大前提，那就是 <strong>ARP 表</strong>。</p>
<p>在一个局域网内，每个网络设备都自己维护了一个 ARP 表，ARP 表记录了某些其他网络设备的 IP 地址-MAC 地址映射关系，该映射关系以 <code>&lt;IP, MAC, TTL&gt;</code> 三元组的形式存储。其中，&#x3D;&#x3D;TTL 为该映射关系的生存周期&#x3D;&#x3D;，典型值为 20 分钟，超过该时间，该条目将被丢弃。</p>
<p>ARP 的工作原理将分两种场景讨论：</p>
<ol>
<li><strong>同一局域网内的 MAC 寻址</strong>；</li>
<li><strong>从一个局域网到另一个局域网中的网络设备的寻址</strong>。</li>
</ol>
<hr>
<h4 id="同一局域网内的-MAC-寻址"><a href="#同一局域网内的-MAC-寻址" class="headerlink" title="同一局域网内的 MAC 寻址"></a>同一局域网内的 MAC 寻址</h4><blockquote>
<p>当主机发送 IP 数据报文时（网络层），仅知道目的地的 IP 地址，并不清楚目的地的 MAC 地址，而 ARP 协议就是解决这一问题的</p>
</blockquote>
<p>为了达成这一目标，主机 A 将不得不通过 ARP 协议来获取主机 B 的 MAC 地址，并将 IP 报文封装成链路层帧，发送到下一跳上。在该局域网内，关于此将按照时间顺序，依次发生如下事件：</p>
<ol>
<li><p>主机 A 检索自己的 ARP 表，发现 ARP 表中并无主机 B 的 IP 地址对应的映射条目，也就无从知道主机 B 的 MAC 地址。</p>
</li>
<li><p>主机 A 将构造一个 ARP 查询分组，并将其广播到所在的局域网中。</p>
<p>ARP 分组是一种特殊报文，ARP 分组有两类，一种是查询分组，另一种是响应分组，它们具有相同的格式，均包含了发送和接收的 IP 地址、发送和接收的 MAC 地址。当然了，查询分组中，发送的 IP 地址，即为主机 A 的 IP 地址，接收的 IP 地址即为主机 B 的 IP 地址，发送的 MAC 地址也是主机 A 的 MAC 地址，但接收的 MAC 地址绝不会是主机 B 的 MAC 地址（因为这正是我们要问询的！），而是一个特殊值——<code>FF-FF-FF-FF-FF-FF</code>，之前说过，该 MAC 地址是广播地址，也就是说，查询分组将广播给该局域网内的所有设备。</p>
</li>
<li><p>主机 A 构造的查询分组将在该局域网内广播，理论上，每一个设备都会收到该分组，并检查查询分组的接收 IP 地址是否为自己的 IP 地址，如果是，说明查询分组已经到达了主机 B，否则，该查询分组对当前设备无效，丢弃之。</p>
</li>
<li><p>主机 B 收到了查询分组之后，验证是对自己的问询，接着构造一个 ARP 响应分组，该分组的目的地只有一个——主机 A，发送给主机 A。同时，主机 B 提取查询分组中的 IP 地址和 MAC 地址信息，在自己的 ARP 表中构造一条主机 A 的 IP-MAC 映射记录。</p>
<p>ARP 响应分组具有和 ARP 查询分组相同的构造，不同的是，发送和接受的 IP 地址恰恰相反，发送的 MAC 地址为发送者本身，目标 MAC 地址为查询分组的发送者，也就是说，ARP 响应分组只有一个目的地，而非广播。</p>
</li>
<li><p>主机 A 终将收到主机 B 的响应分组，提取出该分组中的 IP 地址和 MAC 地址后，构造映射信息，加入到自己的 ARP 表中。</p>
</li>
</ol>
<hr>
<h4 id="不同局域网内的-MAC-寻址"><a href="#不同局域网内的-MAC-寻址" class="headerlink" title="不同局域网内的 MAC 寻址"></a>不同局域网内的 MAC 寻址</h4><p>在讨论 ARP 表时，路由器的多个接口都各自维护一个 ARP 表，而非一个路由器只维护一个 ARP 表。</p>
<ol>
<li>主机 A 查询 ARP 表，期望寻找到目标路由器的本子网接口的 MAC 地址。</li>
<li>目标路由器指的是，根据目的主机 B 的 IP 地址，分析出 B 所在的子网，能够把报文转发到 B 所在子网的那个路由器。</li>
<li>主机 A 未能找到目标路由器的本子网接口的 MAC 地址，将采用 ARP 协议，问询到该 MAC 地址，由于目标接口与主机 A 在同一个子网内，该过程与同一局域网内的 MAC 寻址相同。</li>
<li>主机 A 获取到目标接口的 MAC 地址，先构造 IP 数据报，其中源 IP 是 A 的 IP 地址，目的 IP 地址是 B 的 IP 地址，再构造链路层帧，其中源 MAC 地址是 A 的 MAC 地址，目的 MAC 地址是<strong>本子网内与路由器连接的接口的 MAC 地址</strong>。主机 A 将把这个链路层帧，以单播的方式，发送给目标接口。</li>
<li>目标接口接收到了主机 A 发过来的链路层帧，解析，根据目的 IP 地址，查询转发表，将该 IP 数据报转发到与主机 B 所在子网相连的接口上。</li>
<li>到此，该帧已经从主机 A 所在的子网，转移到了主机 B 所在的子网了。</li>
<li>路由器接口查询 ARP 表，期望寻找到主机 B 的 MAC 地址。</li>
<li>路由器接口如未能找到主机 B 的 MAC 地址，将采用 ARP 协议，广播问询，单播响应，获取到主机 B 的 MAC 地址。</li>
<li>路由器接口将对 IP 数据报重新封装成链路层帧，目标 MAC 地址为主机 B 的 MAC 地址，单播发送，直到目的地。</li>
</ol>
<p><img src="https://javaguide.cn/assets/arp_different_lan.ad156523.png" alt="img"></p>
<hr>
<h2 id="《计算机网络》（谢希仁）内容总结"><a href="#《计算机网络》（谢希仁）内容总结" class="headerlink" title="《计算机网络》（谢希仁）内容总结"></a>《计算机网络》（谢希仁）内容总结</h2><h3 id="1-计算机网络概述"><a href="#1-计算机网络概述" class="headerlink" title="1. 计算机网络概述"></a>1. 计算机网络概述</h3><h4 id="1-1-基本术语"><a href="#1-1-基本术语" class="headerlink" title="1.1. 基本术语"></a>1.1. 基本术语</h4><ol>
<li><strong>结点 （node）</strong> ：网络中的结点可以是计算机，集线器，交换机或路由器等。</li>
<li><strong>链路（link ）</strong> : 从一个结点到另一个结点的一段物理线路。中间没有任何其他交点。</li>
<li><strong>主机（host）</strong> ：连接在因特网上的计算机。</li>
<li><strong>ISP（Internet Service Provider）</strong> ：因特网服务提供者（提供商）。</li>
<li><strong>IXP（Internet eXchange Point）</strong> ： 互联网交换点 IXP 的主要作用就是允许两个网络直接相连并交换分组，而不需要再通过第三个网络来转发分组。</li>
<li><strong>RFC(Request For Comments)</strong> ：意思是“请求评议”，包含了关于 Internet 几乎所有的重要的文字资料。</li>
<li><strong>广域网 WAN（Wide Area Network）</strong> ：任务是通过长距离运送主机发送的数据。</li>
<li><strong>城域网 MAN（Metropolitan Area Network）</strong>：用来将多个局域网进行互连。</li>
<li><strong>局域网 LAN（Local Area Network）</strong> ： 学校或企业大多拥有多个互连的局域网。</li>
<li><strong>个人区域网 PAN（Personal Area Network）</strong> ：在个人工作的地方把属于个人使用的电子设备用无线技术连接起来的网络 。</li>
<li><strong>分组（packet ）</strong> ：因特网中传送的数据单元。由首部 header 和数据段组成。分组又称为包，首部可称为包头</li>
<li><strong>存储转发（store and forward ）</strong> ：路由器收到一个分组，先检查分组是否正确，并过滤掉冲突包错误。确定包正确后，取出目的地址，通过查找表找到想要发送的输出端口地址，然后将该包发送出去。</li>
<li><strong>带宽（bandwidth）</strong> ：在计算机网络中，表示在单位时间内从网络中的某一点到另一点所能通过的“最高数据率”。常用来表示网络的通信线路所能传送数据的能力。单位是“比特每秒”，记为 b&#x2F;s。</li>
<li><strong>吞吐量（throughput ）</strong> ：表示在单位时间内通过某个网络（或信道、接口）的数据量。吞吐量更经常地用于对现实世界中的网络的一种测量，以便知道实际上到底有多少数据量能够通过网络。吞吐量受网络的带宽或网络的额定速率的限制。</li>
</ol>
<hr>
<h4 id="1-2-重要知识点总结"><a href="#1-2-重要知识点总结" class="headerlink" title="1.2. 重要知识点总结"></a>1.2. 重要知识点总结</h4><ol>
<li><strong>计算机网络（简称网络）把许多计算机连接在一起，而互联网把许多网络连接在一起，是网络的网络。</strong></li>
<li>小写字母 i 开头的 internet（互联网）是通用名词，它泛指由多个计算机网络相互连接而成的网络。在这些网络之间的通信协议（即通信规则）可以是任意的。大写字母 I 开头的 Internet（互联网）是专用名词，它指全球最大的，开放的，由众多网络相互连接而成的特定的互联网，并采用 TCP&#x2F;IP 协议作为通信规则，其前身为 ARPANET。Internet 的推荐译名为因特网，现在一般流行称为互联网。</li>
<li>路由器是实现分组交换的关键构件，其任务是转发收到的分组，这是网络核心部分最重要的功能。分组交换采用存储转发技术，表示把一个报文（要发送的整块数据）分为几个分组后再进行传送。在发送报文之前，先把较长的报文划分成为一个个更小的等长数据段。在每个数据端的前面加上一些由必要的控制信息组成的首部后，就构成了一个分组。分组又称为包。分组是在互联网中传送的数据单元，正是由于分组的头部包含了诸如目的地址和源地址等重要控制信息，每一个分组才能在互联网中独立的选择传输路径，并正确地交付到分组传输的终点。</li>
<li>互联网按工作方式可划分为边缘部分和核心部分。主机在网络的边缘部分，其作用是进行信息处理。由大量网络和连接这些网络的路由器组成核心部分，其作用是提供连通性和交换。</li>
<li>计算机通信是计算机中进程（即运行着的程序）之间的通信。计算机网络采用的通信方式是客户-服务器方式（C&#x2F;S 方式）和对等连接方式（P2P 方式）。</li>
<li>客户和服务器都是指通信中所涉及的应用进程。客户是服务请求方，服务器是服务提供方。</li>
<li>按照作用范围的不同，计算机网络分为广域网 WAN，城域网 MAN，局域网 LAN，个人区域网 PAN。</li>
<li><strong>计算机网络最常用的性能指标是：速率，带宽，吞吐量，时延（发送时延，处理时延，排队时延），时延带宽积，往返时间和信道利用率。</strong></li>
<li>网络协议即协议，是为进行网络中的数据交换而建立的规则。计算机网络的各层以及其协议集合，称为网络的体系结构。</li>
<li><strong>五层体系结构由应用层，运输层，网络层（网际层），数据链路层，物理层组成。运输层最主要的协议是 TCP 和 UDP 协议，网络层最重要的协议是 IP 协议。</strong></li>
</ol>
<hr>
<h3 id="2-物理层（Physical-Layer）"><a href="#2-物理层（Physical-Layer）" class="headerlink" title="2. 物理层（Physical Layer）"></a>2. 物理层（Physical Layer）</h3><h4 id="2-1-基本术语"><a href="#2-1-基本术语" class="headerlink" title="2.1. 基本术语"></a>2.1. 基本术语</h4><ol>
<li><strong>数据（data）</strong> :运送消息的实体。</li>
<li><strong>信号（signal）</strong> ：数据的电气的或电磁的表现。或者说信号是适合在传输介质上传输的对象。</li>
<li><strong>码元（ code）</strong> ：在使用时间域（或简称为时域）的波形来表示数字信号时，代表不同离散数值的基本波形。</li>
<li><strong>单工（simplex ）</strong> : 只能有一个方向的通信而没有反方向的交互。</li>
<li><strong>半双工（half duplex ）</strong> ：通信的双方都可以发送信息，但不能双方同时发送(当然也就不能同时接收)。</li>
<li><strong>全双工（full duplex）</strong> : 通信的双方可以同时发送和接收信息。</li>
<li><strong>失真</strong>：失去真实性，主要是指接受到的信号和发送的信号不同，有磨损和衰减。影响失真程度的因素：1.码元传输速率 2.信号传输距离 3.噪声干扰 4.传输媒体质量</li>
<li><strong>奈氏准则</strong> : 在任何信道中，码元的传输的效率是有上限的，传输速率超过此上限，就会出现严重的码间串扰问题，使接收端对码元的判决（即识别）成为不可能。</li>
<li><strong>香农定理</strong> ：在带宽受限且有噪声的信道中，为了不产生误差，信息的数据传输速率有上限值。</li>
<li><strong>基带信号（baseband signal）</strong> : 来自信源的信号。指没有经过调制的数字信号或模拟信号。</li>
<li><strong>带通（频带）信号（bandpass signal）</strong> ：把基带信号经过载波调制后，把信号的频率范围搬移到较高的频段以便在信道中传输（即仅在一段频率范围内能够通过信道），这里调制过后的信号就是带通信号。</li>
<li><strong>调制（modulation ）</strong> : 对信号源的信息进行处理后加到载波信号上，使其变为适合在信道传输的形式的过程。</li>
<li><strong>信噪比（signal-to-noise ratio ）</strong> : 指信号的平均功率和噪声的平均功率之比，记为 S&#x2F;N。信噪比（dB）&#x3D;10*log10（S&#x2F;N）。</li>
<li><strong>信道复用（channel multiplexing ）</strong> ：指多个用户共享同一个信道。（并不一定是同时）。</li>
<li><strong>比特率（bit rate ）</strong> ：单位时间（每秒）内传送的比特数。</li>
<li><strong>波特率（baud rate）</strong> ：单位时间载波调制状态改变的次数。针对数据信号对载波的调制速率。</li>
<li><strong>复用（multiplexing）</strong> ：共享信道的方法。</li>
<li><strong>ADSL（Asymmetric Digital Subscriber Line ）</strong> ：非对称数字用户线。</li>
<li><strong>光纤同轴混合网（HFC 网）</strong> :在目前覆盖范围很广的有线电视网的基础上开发的一种居民宽带接入网</li>
</ol>
<hr>
<h4 id="2-2-重要知识点总结"><a href="#2-2-重要知识点总结" class="headerlink" title="2.2. 重要知识点总结"></a>2.2. 重要知识点总结</h4><ol>
<li><strong>物理层的主要任务就是确定与传输媒体接口有关的一些特性，如机械特性，电气特性，功能特性，过程特性。</strong></li>
<li><strong>通信的目的是传送消息。如话音，文字，图像等都是消息，数据是运送消息的实体。信号则是数据的电气或电磁的表现。</strong></li>
</ol>
<hr>
<h4 id="2-3-补充"><a href="#2-3-补充" class="headerlink" title="2.3. 补充"></a>2.3. 补充</h4><h5 id="2-3-1-物理层主要做啥？"><a href="#2-3-1-物理层主要做啥？" class="headerlink" title="2.3.1. 物理层主要做啥？"></a>2.3.1. 物理层主要做啥？</h5><p>物理层主要做的事情就是 <strong>透明地传送比特流</strong>。<strong>物理层考虑的是怎样才能在连接各种计算机的传输媒体上传输数据比特流，而不是指具体的传输媒体。</strong> 现有的计算机网络中的硬件设备和传输媒体的种类非常繁多，而且通信手段也有许多不同的方式。物理层的作用正是尽可能地屏蔽掉这些传输媒体和通信手段的差异，使物理层上面的数据链路层感觉不到这些差异，这样就可以使数据链路层只考虑完成本层的协议和服务，而不必考虑网络的具体传输媒体和通信手段是什么。</p>
<hr>
<h5 id="2-3-2-几种常用的信道复用技术"><a href="#2-3-2-几种常用的信道复用技术" class="headerlink" title="2.3.2. 几种常用的信道复用技术"></a>2.3.2. 几种常用的信道复用技术</h5><ol>
<li><strong>频分复用(FDM)</strong> ：所有用户在同样的时间占用不同的带宽资源。</li>
<li><strong>时分复用（TDM）</strong> ：所有用户在不同的时间占用同样的频带宽度（分时不分频）。</li>
<li><strong>统计时分复用 (Statistic TDM)</strong> ：改进的时分复用，能够明显提高信道的利用率。</li>
<li><strong>码分复用(CDM)</strong> ： 用户使用经过特殊挑选的不同码型，因此各用户之间不会造成干扰。这种系统发送的信号有很强的抗干扰能力，其频谱类似于白噪声，不易被敌人发现。</li>
<li><strong>波分复用( WDM)</strong> ：波分复用就是光的频分复用。2.3.3. 几种常用的宽带接入技术，主要是 ADSL 和 FTTx</li>
</ol>
<hr>
<h3 id="3-数据链路层（Data-Link-Layer）"><a href="#3-数据链路层（Data-Link-Layer）" class="headerlink" title="3. 数据链路层（Data Link Layer）"></a>3. 数据链路层（Data Link Layer）</h3><h4 id="3-1-基本术语"><a href="#3-1-基本术语" class="headerlink" title="3.1. 基本术语"></a>3.1. 基本术语</h4><ol>
<li><p><strong>链路（link）</strong> ：一个结点到相邻结点的一段物理链路。</p>
</li>
<li><p><strong>数据链路（data link）</strong> ：把实现控制数据运输的协议的硬件和软件加到链路上就构成了数据链路。</p>
</li>
<li><p><strong>循环冗余检验 CRC（Cyclic Redundancy Check）</strong> ：为了保证数据传输的可靠性，CRC 是数据链路层广泛使用的一种检错技术。</p>
</li>
<li><p><strong>帧（frame）</strong> ：一个数据链路层的传输单元，由一个数据链路层首部和其携带的封包所组成协议数据单元。</p>
</li>
<li><p><strong>MTU（Maximum Transfer Uint ）</strong> ：最大传送单元。帧的数据部分的的长度上限。</p>
</li>
<li><p><strong>误码率 BER（Bit Error Rate ）</strong> ：在一段时间内，传输错误的比特占所传输比特总数的比率。</p>
</li>
<li><p><strong>PPP（Point-to-Point Protocol ）</strong> ：点对点协议。即用户计算机和 ISP 进行通信时所使用的数据链路层协议。以下是 PPP 帧的示意</p>
</li>
<li><p><strong>MAC 地址（Media Access Control 或者 Medium Access Control）</strong> ：意译为媒体访问控制，或称为物理地址、硬件地址，用来定义网络设备的位置。在 OSI 模型中，第三层网络层负责 IP 地址，第二层数据链路层则负责 MAC 地址。因此一个主机会有一个 MAC 地址，而每个网络位置会有一个专属于它的 IP 地址 。地址是识别某个系统的重要标识符，“名字指出我们所要寻找的资源，地址指出资源所在的地方，路由告诉我们如何到达该处。”</p>
</li>
<li><p><strong>网桥（bridge）</strong> ：一种用于数据链路层实现中继，连接两个或多个局域网的网络互连设备。</p>
</li>
<li><p><strong>交换机（switch ）</strong> ：广义的来说，交换机指的是一种通信系统中完成信息交换的设备。这里工作在数据链路层的交换机指的是交换式集线器，其实质是一个多接口的网桥</p>
<hr>
</li>
</ol>
<h4 id="3-2-重要知识点总结"><a href="#3-2-重要知识点总结" class="headerlink" title="3.2. 重要知识点总结"></a>3.2. 重要知识点总结</h4><ol>
<li><p>数据链路层使用的主要是<strong>点对点信道</strong>和<strong>广播信道</strong>两种。</p>
</li>
<li><p><strong>循环冗余检验 CRC</strong> 是一种检错方法，而帧检验序列 FCS 是添加在数据后面的冗余码</p>
</li>
<li><p><strong>点对点协议 PPP</strong> 是数据链路层使用最多的一种协议，它的特点是：简单，只检测差错而不去纠正差错，不使用序号，也不进行流量控制，可同时支持多种网络层协议</p>
</li>
<li><p><strong>局域网的优点是：具有广播功能，从一个站点可方便地访问全网；便于系统的扩展和逐渐演变；提高了系统的可靠性，可用性和生存性。</strong></p>
</li>
<li><p>计算机与外接局域网通信需要通过通信适配器（或网络适配器），它又称为网络接口卡或网卡。<strong>计算器的硬件地址就在适配器的 ROM 中</strong>。</p>
</li>
<li><p>以太网采用的协议是具有冲突检测的<strong>载波监听多点接入 CSMA&#x2F;CD</strong>。协议的特点是：<strong>发送前先监听，边发送边监听，一旦发现总线上出现了碰撞，就立即停止发送。然后按照退避算法等待一段随机时间后再次发送。</strong> 因此，每一个站点在自己发送数据之后的一小段时间内，存在着遭遇碰撞的可能性。以太网上的各站点平等地争用以太网信道</p>
<hr>
</li>
</ol>
<h3 id="4-网络层（Network-Layer）"><a href="#4-网络层（Network-Layer）" class="headerlink" title="4. 网络层（Network Layer）"></a>4. 网络层（Network Layer）</h3><h4 id="4-1-基本术语"><a href="#4-1-基本术语" class="headerlink" title="4.1. 基本术语"></a>4.1. 基本术语</h4><ol>
<li><strong>虚电路（Virtual Circuit）</strong> : 在两个终端设备的逻辑或物理端口之间，通过建立的双向的透明传输通道。虚电路表示这只是一条逻辑上的连接，分组都沿着这条逻辑连接按照存储转发方式传送，而并不是真正建立了一条物理连接。</li>
<li><strong>IP（Internet Protocol ）</strong> : 网际协议 IP 是 TCP&#x2F;IP 体系中两个最主要的协议之一，是 TCP&#x2F;IP 体系结构网际层的核心。配套的有 ARP，RARP，ICMP，IGMP。</li>
<li><strong>ARP（Address Resolution Protocol）</strong> : 地址解析协议。地址解析协议 ARP 把 IP 地址解析为硬件地址。</li>
<li><strong>ICMP（Internet Control Message Protocol ）</strong> ：网际控制报文协议 （ICMP 允许主机或路由器报告差错情况和提供有关异常情况的报告）。</li>
<li><strong>子网掩码（subnet mask ）</strong> ：它是一种用来指明一个 IP 地址的哪些位标识的是主机所在的子网以及哪些位标识的是主机的位掩码。子网掩码不能单独存在，它必须结合 IP 地址一起使用。</li>
<li><strong>CIDR（ Classless Inter-Domain Routing ）</strong>：无分类域间路由选择 （特点是消除了传统的 A 类、B 类和 C 类地址以及划分子网的概念，并使用各种长度的“网络前缀”(network-prefix)来代替分类地址中的网络号和子网号）。</li>
<li><strong>默认路由（default route）</strong> ：当在路由表中查不到能到达目的地址的路由时，路由器选择的路由。默认路由还可以减小路由表所占用的空间和搜索路由表所用的时间。</li>
<li><strong>路由选择算法（Virtual Circuit）</strong> ：路由选择协议的核心部分。因特网采用自适应的，分层次的路由选择协议</li>
</ol>
<hr>
<h4 id="4-2-重要知识点总结"><a href="#4-2-重要知识点总结" class="headerlink" title="4.2. 重要知识点总结"></a>4.2. 重要知识点总结</h4><ol>
<li>TCP&#x2F;IP 协议中的网络层向上只提供简单灵活的，无连接的，尽最大努力交付的数据报服务。网络层不提供服务质量的承诺，不保证分组交付的时限所传送的分组可能出错，丢失，重复和失序。进程之间通信的可靠性由运输层负责</li>
<li>地址解析协议 ARP 把 IP 地址解析为硬件地址。ARP 的高速缓存可以大大减少网络上的通信量。因为这样可以使主机下次再与同样地址的主机通信时，可以直接从高速缓存中找到所需要的硬件地址而不需要再去以广播方式发送 ARP 请求分组</li>
<li><strong>要解决 IP 地址耗尽的问题，最根本的办法是采用具有更大地址空间的新版本 IP 协议-IPv6。</strong> IPv6 所带来的变化有 ① 更大的地址空间（采用 128 位地址）② 灵活的首部格式 ③ 改进的选项 ④ 支持即插即用 ⑤ 支持资源的预分配 ⑥IPv6 的首部改为 8 字节对齐。</li>
<li>虚拟专用网络 VPN 利用公用的互联网作为本机构专用网之间的通信载体。VPN 内使用互联网的专用地址。一个 VPN 至少要有一个路由器具有合法的全球 IP 地址，这样才能和本系统的另一个 VPN 通过互联网进行通信。所有通过互联网传送的数据都需要加密。</li>
</ol>
<hr>
<h3 id="5-传输层（Transport-Layer）"><a href="#5-传输层（Transport-Layer）" class="headerlink" title="5. 传输层（Transport Layer）"></a>5. 传输层（Transport Layer）</h3><h4 id="5-1-基本术语"><a href="#5-1-基本术语" class="headerlink" title="5.1. 基本术语"></a>5.1. 基本术语</h4><ol>
<li><strong>进程（process）</strong> ：指计算机中正在运行的程序实体。</li>
<li><strong>应用进程互相通信</strong> ：一台主机的进程和另一台主机中的一个进程交换数据的过程（另外注意通信真正的端点不是主机而是主机中的进程，也就是说端到端的通信是应用进程之间的通信）。</li>
<li><strong>传输层的复用与分用</strong> ：复用指发送方不同的进程都可以通过同一个运输层协议传送数据。分用指接收方的运输层在剥去报文的首部后能把这些数据正确的交付到目的应用进程。</li>
<li><strong>TCP（Transmission Control Protocol）</strong> ：传输控制协议。</li>
<li><strong>UDP（User Datagram Protocol）</strong> ：用户数据报协议。</li>
<li><strong>端口（port）</strong> ：端口的目的是为了确认对方机器的哪个进程在与自己进行交互，比如 MSN 和 QQ 的端口不同，如果没有端口就可能出现 QQ 进程和 MSN 交互错误。端口又称协议端口号。</li>
<li><strong>停止等待协议（stop-and-wait）</strong> ：指发送方每发送完一个分组就停止发送，等待对方确认，在收到确认之后在发送下一个分组。</li>
<li><strong>流量控制</strong> : 就是让发送方的发送速率不要太快，既要让接收方来得及接收，也不要使网络发生拥塞。</li>
<li><strong>拥塞控制</strong> ：防止过多的数据注入到网络中，这样可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提，就是网络能够承受现有的网络负荷。</li>
</ol>
<hr>
<h4 id="5-2-重要知识点总结"><a href="#5-2-重要知识点总结" class="headerlink" title="5.2. 重要知识点总结"></a>5.2. 重要知识点总结</h4><ol>
<li><p>运输层提供应用进程之间的逻辑通信，也就是说，运输层之间的通信并不是真正在两个运输层之间直接传输数据。运输层向应用层屏蔽了下面网络的细节（如网络拓补，所采用的路由选择协议等），它使应用进程之间看起来好像两个运输层实体之间有一条端到端的逻辑通信信道。</p>
</li>
<li><p><strong>网络层为主机提供逻辑通信，而运输层为应用进程之间提供端到端的逻辑通信。</strong></p>
</li>
<li><p>UDP 在传送数据之前不需要先建立连接，远地主机在收到 UDP 报文后，不需要给出任何确认。虽然 UDP 不提供可靠交付，但在某些情况下 UDP 确是一种最有效的工作方式。 TCP 提供面向连接的服务。在传送数据之前必须先建立连接，数据传送结束后要释放连接。TCP 不提供广播或多播服务。由于 TCP 要提供可靠的，面向连接的传输服务，难以避免地增加了许多开销，如确认，流量控制，计时器以及连接管理等。这不仅使协议数据单元的首部增大很多，还要占用许多处理机资源。</p>
</li>
<li><p><strong>UDP 的主要特点是 ① 无连接 ② 尽最大努力交付 ③ 面向报文 ④ 无拥塞控制 ⑤ 支持一对一，一对多，多对一和多对多的交互通信 ⑥ 首部开销小（只有四个字段：源端口，目的端口，长度和检验和）</strong></p>
</li>
<li><p><strong>TCP 的主要特点是 ① 面向连接 ② 每一条 TCP 连接只能是一对一的 ③ 提供可靠交付 ④ 提供全双工通信 ⑤ 面向字节流</strong></p>
</li>
<li><p><strong>TCP 用主机的 IP 地址加上主机上的端口号作为 TCP 连接的端点。这样的端点就叫做套接字（socket）或插口。套接字用（IP 地址：端口号）来表示。每一条 TCP 连接唯一地被通信两端的两个端点所确定。</strong></p>
</li>
<li><p><strong>TCP 使用滑动窗口机制。发送窗口里面的序号表示允许发送的序号。发送窗口后沿的后面部分表示已发送且已收到确认，而发送窗口前沿的前面部分表示不允许发送。发送窗口后沿的变化情况有两种可能，即不动（没有收到新的确认）和前移（收到了新的确认）。发送窗口的前沿通常是不断向前移动的。一般来说，我们总是希望数据传输更快一些。但如果发送方把数据发送的过快，接收方就可能来不及接收，这就会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快，要让接收方来得及接收。</strong></p>
</li>
<li><p><strong>为了进行拥塞控制，TCP 发送方要维持一个拥塞窗口 cwnd 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度，并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。</strong></p>
</li>
<li><p><strong>TCP 的拥塞控制采用了四种算法，即慢开始，拥塞避免，快重传和快恢复。在网络层也可以使路由器采用适当的分组丢弃策略（如&#x3D;&#x3D;主动队列管理 AQM（当队列长度达到某个阈值时，主动丢弃到达的分组）&#x3D;&#x3D;），以减少网络拥塞的发生。</strong></p>
</li>
<li><p><strong>主动发起 TCP 连接建立的应用进程叫做客户，而被动等待连接建立的应用进程叫做服务器。TCP 连接采用三报文握手机制。服务器要确认用户的连接请求，然后客户要对服务器的确认进行确认。</strong></p>
</li>
</ol>
<h5 id="x3D-x3D-主动队列管理（AQM）-x3D-x3D"><a href="#x3D-x3D-主动队列管理（AQM）-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;主动队列管理（AQM）&#x3D;&#x3D;"></a>&#x3D;&#x3D;主动队列管理（AQM）&#x3D;&#x3D;</h5><p>路由器的尾部丢弃往往会导致一连串分组的丢失，这就使发送方出现超时重传，使TCP进入拥塞控制的慢开始状态，结果使TCP连接的发送方突然把数据的发送速率降低到很小的数值。更为严重的是，在网络中通常有很多的TCP连接（它们有不同的源点和终点），这些连接中的报文段通常是复用在网络层的IP数据报中传送。在这种情况下，若发生了路由器中的尾部丢弃，就可能会同时影响到很多条TCP连接，结果使这许多TCP连接在同一时间突然都进入到慢开始状态。这在TCP的术语中称为全局同步。全局同步使得全网的通信量突然下降了很多，而在网络恢复正常后，其通信量又突然增大很多。</p>
<hr>
<h4 id="5-3-补充（重要）"><a href="#5-3-补充（重要）" class="headerlink" title="5.3. 补充（重要）"></a>5.3. 补充（重要）</h4><ol>
<li><p>端口和套接字的意义</p>
<ol>
<li>套接字：进程通过一个为套接字的软件接口向网络发送报文和从网络接受报文。IP+端口</li>
<li>进程：用于标识进程的标识符就是端口号。</li>
</ol>
</li>
<li><p>UDP 和 TCP 的区别以及两者的应用场景</p>
<ol>
<li><a target="_blank" rel="noopener" href="https://zhuanlan.zhihu.com/p/108579426">https://zhuanlan.zhihu.com/p/108579426</a></li>
</ol>
</li>
<li><p>在不可靠的网络上实现可靠传输的工作原理，停止等待协议和 ARQ（自动重传请求） 协议</p>
</li>
<li><p>TCP 的滑动窗口，流量控制，拥塞控制和连接管理</p>
</li>
<li><p>TCP 的三次握手，四次挥手机制</p>
</li>
</ol>
<hr>
<h3 id="6-应用层（Application-Layer）"><a href="#6-应用层（Application-Layer）" class="headerlink" title="6. 应用层（Application Layer）"></a>6. 应用层（Application Layer）</h3><h4 id="6-1-基本术语"><a href="#6-1-基本术语" class="headerlink" title="6.1. 基本术语"></a>6.1. 基本术语</h4><ol>
<li><strong>域名系统（DNS）</strong> ：域名系统（DNS，Domain Name System）将人类可读的域名 (例如，<a target="_blank" rel="noopener" href="http://www.baidu.com/">www.baidu.com</a>) 转换为机器可读的 IP 地址 (例如，220.181.38.148)。我们可以将其理解为专为互联网设计的电话薄。</li>
<li><strong>文件传输协议（FTP）</strong> ：FTP 是 File Transfer Protocol（文件传输协议）的英文简称，而中文简称为“文传协议”。用于 Internet 上的控制文件的双向传输。同时，它也是一个应用程序（Application）。基于不同的操作系统有不同的 FTP 应用程序，而所有这些应用程序都遵守同一种协议以传输文件。在 FTP 的使用当中，用户经常遇到两个概念：”下载”（Download）和”上传”（Upload）。 “下载”文件就是从远程主机拷贝文件至自己的计算机上；”上传”文件就是将文件从自己的计算机中拷贝至远程主机上。用 Internet 语言来说，用户可通过客户机程序向（从）远程主机上传（下载）文件。</li>
<li><strong>简单文件传输协议（TFTP）</strong> ：TFTP（Trivial File Transfer Protocol,简单文件传输协议）是 TCP&#x2F;IP 协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议，提供不复杂、开销不大的文件传输服务。端口号为 69。</li>
<li><strong>远程终端协议（TELNET）</strong> ：Telnet 协议是 TCP&#x2F;IP 协议族中的一员，是 Internet 远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。在终端使用者的电脑上使用 telnet 程序，用它连接到服务器。终端使用者可以在 telnet 程序中输入命令，这些命令会在服务器上运行，就像直接在服务器的控制台上输入一样。可以在本地就能控制服务器。要开始一个 telnet 会话，必须输入用户名和密码来登录服务器。Telnet 是常用的远程控制 Web 服务器的方法。</li>
<li><strong>万维网（WWW）</strong> ：WWW 是环球信息网的缩写，（亦作“Web”、“WWW”、“’W3’”，英文全称为“World Wide Web”），中文名字为“万维网”，”环球网”等，常简称为 Web。分为 Web 客户端和 Web 服务器程序。WWW 可以让 Web 客户端（常用浏览器）访问浏览 Web 服务器上的页面。是一个由许多互相链接的超文本组成的系统，通过互联网访问。在这个系统中，每个有用的事物，称为一样“资源”；并且由一个全局“统一资源标识符”（URI）标识；这些资源通过超文本传输协议（Hypertext Transfer Protocol）传送给用户，而后者通过点击链接来获得资源。万维网联盟（英语：World Wide Web Consortium，简称 W3C），又称 W3C 理事会。1994 年 10 月在麻省理工学院（MIT）计算机科学实验室成立。万维网联盟的创建者是万维网的发明者蒂姆·伯纳斯-李。万维网并不等同互联网，万维网只是互联网所能提供的服务其中之一，是靠着互联网运行的一项服务。</li>
<li><strong>万维网的大致工作工程：</strong><img src="https://img-blog.csdnimg.cn/img_convert/735f55501e81898aa61b8032f7dbcb73.png" alt="万维网的大致工作工程"></li>
<li><strong>统一资源定位符（URL）</strong> ：统一资源定位符是对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示，是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的 URL，它包含的信息指出文件的位置以及浏览器应该怎么处理它。</li>
<li><strong>超文本传输协议（HTTP）</strong> ：超文本传输协议（HTTP，HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的 WWW 文件都必须遵守这个标准。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。1960 年美国人 Ted Nelson 构思了一种通过计算机处理文本信息的方法，并称之为超文本（hypertext）,这成为了 HTTP 超文本传输协议标准架构的发展根基。HTTP 协议的本质就是一种浏览器与服务器之间约定好的通信格式。HTTP 的原理如下图所示：<img src="https://img-blog.csdnimg.cn/img_convert/b273efef5f2388e26414135672b00295.png" alt="img"></li>
<li><strong>代理服务器（Proxy Server）</strong> ： 代理服务器（Proxy Server）是一种网络实体，它又称为万维网高速缓存。 代理服务器把最近的一些请求和响应暂存在本地磁盘中。当新请求到达时，若代理服务器发现这个请求与暂时存放的的请求相同，就返回暂存的响应，而不需要按 URL 的地址再次去互联网访问该资源。代理服务器可在客户端或服务器工作，也可以在中间系统工作。</li>
<li><strong>简单邮件传输协议(SMTP)</strong> : SMTP（Simple Mail Transfer Protocol）即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则，由它来控制信件的中转方式。 SMTP 协议属于 TCP&#x2F;IP 协议簇，它帮助每台计算机在发送或中转信件时找到下一个目的地。 通过 SMTP 协议所指定的服务器,就可以把 E-mail 寄到收信人的服务器上了，整个过程只要几分钟。S P 协议的发送邮件服务器，用来发送或中转发出的电子邮件。</li>
<li><strong>搜索引擎</strong> :搜索引擎（Search Engine）是指根据一定的策略、运用特定的计算机程序从互联网上搜集信息，在对信息进行组织和处理后，为用户提供检索服务，将用户检索相关的信息展示给用户的系统。搜索引擎包括全文索引、目录索引、元搜索引擎、垂直搜索引擎、集合式搜索引擎、门户搜索引擎与免费链接列表等。</li>
<li><strong>垂直搜索引擎</strong> ：垂直搜索引擎是针对某一个行业的专业搜索引擎，是搜索引擎的细分和延伸，是对网页库中的某类专门的信息进行一次整合，定向分字段抽取出需要的数据进行处理后再以某种形式返回给用户。垂直搜索是相对通用搜索引擎的信息量大、查询不准确、深度不够等提出来的新的搜索引擎服务模式，通过针对某一特定领域、某一特定人群或某一特定需求提供的有一定价值的信息和相关服务。其特点就是“专、精、深”，且具有行业色彩，相比较通用搜索引擎的海量信息无序化，垂直搜索引擎则显得更加专注、具体和深入。</li>
<li><strong>全文索引</strong> :全文索引技术是目前搜索引擎的关键技术。试想在 1M 大小的文件中搜索一个词，可能需要几秒，在 100M 的文件中可能需要几十秒，如果在更大的文件中搜索那么就需要更大的系统开销，这样的开销是不现实的。所以在这样的矛盾下出现了全文索引技术，有时候有人叫倒排文档技术。</li>
<li><strong>目录索引</strong> ：目录索引（ search index&#x2F;directory)，顾名思义就是将网站分门别类地存放在相应的目录中，因此用户在查询信息时，可选择关键词搜索，也可按分类目录逐层查找。</li>
</ol>
<hr>
<h4 id="6-2-重要知识点总结"><a href="#6-2-重要知识点总结" class="headerlink" title="6.2. 重要知识点总结"></a>6.2. 重要知识点总结</h4><ol>
<li>文件传输协议（FTP）使用 TCP 可靠的运输服务。FTP 使用客户服务器方式。一个 FTP 服务器进程可以同时为多个用户提供服务。在进行文件传输时，FTP 的客户和服务器之间要先建立两个并行的 TCP 连接:控制连接和数据连接。实际用于传输文件的是数据连接。</li>
<li>万维网客户程序与服务器之间进行交互使用的协议是超文本传输协议 HTTP。HTTP 使用 TCP 连接进行可靠传输。但 HTTP 本身是无连接、无状态的。HTTP&#x2F;1.1 协议使用了持续连接（分为非流水线方式和流水线方式）</li>
<li>电子邮件把邮件发送到收件人使用的邮件服务器，并放在其中的收件人邮箱中，收件人可随时上网到自己使用的邮件服务器读取，相当于电子邮箱。</li>
<li>一个电子邮件系统有三个重要组成构件：用户代理、邮件服务器、邮件协议（包括邮件发送协议，如 SMTP，和邮件读取协议，如 POP3 和 IMAP）。用户代理和邮件服务器都要运行这些协议。</li>
</ol>
<hr>
<h4 id="6-3-补充（重要）"><a href="#6-3-补充（重要）" class="headerlink" title="6.3. 补充（重要）"></a>6.3. 补充（重要）</h4><ol>
<li><p>应用层的常见协议（重点关注 HTTP 协议）</p>
</li>
<li><p>域名系统（DNS）-从域名解析出 IP 地址</p>
</li>
<li><p>访问一个网站大致的过程(<a target="_blank" rel="noopener" href="https://blog.csdn.net/u012862311/article/details/78753232">https://blog.csdn.net/u012862311/article/details/78753232</a>)</p>
<ol>
<li>域名解析成IP地址；</li>
<li>与目的主机进行TCP连接（三次握手）；</li>
<li>发送与收取数据（浏览器与目的主机开始HTTP访问过程）；</li>
<li>与目的主机断开TCP连接（四次挥手）；</li>
</ol>
</li>
<li><p>系统调用和应用编程接口概念</p>
</li>
</ol>
<h2 id="重要知识点-2"><a href="#重要知识点-2" class="headerlink" title="重要知识点"></a>重要知识点</h2><h3 id="OSI-和-TCP-x2F-IP-网络分层模型详解（基础）-1"><a href="#OSI-和-TCP-x2F-IP-网络分层模型详解（基础）-1" class="headerlink" title="OSI 和 TCP&#x2F;IP 网络分层模型详解（基础）"></a>OSI 和 TCP&#x2F;IP 网络分层模型详解（基础）</h3><h3 id="网络攻击常见手段总结"><a href="#网络攻击常见手段总结" class="headerlink" title="网络攻击常见手段总结"></a>网络攻击常见手段总结</h3><h4 id="IP-欺骗"><a href="#IP-欺骗" class="headerlink" title="IP 欺骗"></a>IP 欺骗</h4><h5 id="IP-是什么"><a href="#IP-是什么" class="headerlink" title="IP 是什么?"></a>IP 是什么?</h5><p>在网络中，所有的设备都会分配一个地址。这个地址就仿佛小蓝的家地址「<strong>多少号多少室</strong>」，这个号就是分配给整个子网的，「<strong>室</strong>」对应的号码即分配给子网中计算机的，这就是网络中的地址。「号」对应的号码为网络号，「<strong>室</strong>」对应的号码为主机号，这个地址的整体就是 <strong>IP 地址</strong></p>
<hr>
<h5 id="通过-IP-地址我们能知道什么？"><a href="#通过-IP-地址我们能知道什么？" class="headerlink" title="通过 IP 地址我们能知道什么？"></a>通过 IP 地址我们能知道什么？</h5><h5 id="IP-欺骗技术是什么？"><a href="#IP-欺骗技术是什么？" class="headerlink" title="IP 欺骗技术是什么？"></a>IP 欺骗技术是什么？</h5><p>IP 欺骗技术就是<strong>伪造</strong>某台主机的 IP 地址的技术。通过 IP 地址的伪装使得某台主机能够<strong>伪装</strong>另外的一台主机，而这台主机往往具有某种特权或者被另外的主机所信任。</p>
<p>假设现在有一个合法用户 <strong>(1.1.1.1)</strong> 已经同服务器建立正常的连接，攻击者构造攻击的 TCP 数据，伪装自己的 IP 为 <strong>1.1.1.1</strong>，并向服务器发送一个带有 RSI 位的 TCP 数据段。服务器接收到这样的数据后，认为从 <strong>1.1.1.1</strong> 发送的连接有错误，就会清空缓冲区中建立好的连接。</p>
<p>这时，如果合法用户 <strong>1.1.1.1</strong> 再发送合法数据，服务器就已经没有这样的连接了，该用户就必须从新开始建立连接。攻击时，伪造大量的 IP 地址，向目标发送 RST 数据，使服务器不对合法用户服务。虽然 IP 地址欺骗攻击有着相当难度，但我们应该清醒地意识到，这种攻击非常广泛，入侵往往从这种攻击开始。</p>
<hr>
<h5 id="如何缓解-IP-欺骗？"><a href="#如何缓解-IP-欺骗？" class="headerlink" title="如何缓解 IP 欺骗？"></a>如何缓解 IP 欺骗？</h5><p>虽然无法预防 IP 欺骗，但可以采取措施来阻止伪造数据包渗透网络。<strong>入口过滤</strong> 是防范欺骗的一种极为常见的防御措施，如 BCP38（通用最佳实践文档）所示。入口过滤是一种数据包过滤形式，通常在<a target="_blank" rel="noopener" href="https://www.cloudflare.com/learning/serverless/glossary/what-is-edge-computing/">网络边缘open in new window</a>设备上实施，用于检查传入的 IP 数据包并确定其源标头。如果这些数据包的源标头与其来源不匹配或者看上去很可疑，则拒绝这些数据包。一些网络还实施出口过滤，检查退出网络的 IP 数据包，确保这些数据包具有合法源标头，以防止网络内部用户使用 IP 欺骗技术发起出站恶意攻击。</p>
<hr>
<h2 id="操作系统"><a href="#操作系统" class="headerlink" title="操作系统"></a>操作系统</h2><h3 id="操作系统常见面试题总结"><a href="#操作系统常见面试题总结" class="headerlink" title="操作系统常见面试题总结"></a>操作系统常见面试题总结</h3><h4 id="操作系统基础"><a href="#操作系统基础" class="headerlink" title="操作系统基础"></a>操作系统基础</h4><h5 id="什么是操作系统？"><a href="#什么是操作系统？" class="headerlink" title="什么是操作系统？"></a>什么是操作系统？</h5><ol>
<li><strong>操作系统（Operating System，简称 OS）是管理计算机硬件与软件资源的程序，是计算机的基石。</strong></li>
<li><strong>操作系统本质上是一个运行在计算机上的软件程序 ，用于管理计算机硬件和软件资源。</strong> 举例：运行在你电脑上的所有应用程序都通过操作系统来调用系统内存以及磁盘等等硬件。</li>
<li><strong>操作系统存在屏蔽了硬件层的复杂性。</strong> 操作系统就像是硬件使用的负责人，统筹着各种相关事项。</li>
<li><strong>操作系统的内核（Kernel）是操作系统的核心部分，它负责系统的内存管理，硬件设备的管理，文件系统的管理以及应用程序的管理</strong>。 内核是连接应用程序和硬件的桥梁，决定着系统的性能和稳定性。</li>
</ol>
<hr>
<h5 id="系统调用"><a href="#系统调用" class="headerlink" title="系统调用"></a>系统调用</h5><p>根据进程访问资源的特点，我们可以把进程在系统上的运行分为两个级别：</p>
<ol>
<li>用户态(user mode) : 用户态运行的进程可以直接读取用户程序的数据。</li>
<li>系统态(kernel mode):可以简单的理解系统态运行的进程或程序几乎可以访问计算机的任何资源，不受限制。</li>
</ol>
<p>说了用户态和系统态之后，那么什么是系统调用呢？</p>
<p>我们运行的程序基本都是运行在用户态，如果我们调用操作系统提供的系统态级别的子功能咋办呢？那就需要系统调用了！</p>
<p>也就是说在我们运行的用户程序中，凡是与系统态级别的资源有关的操作（如文件管理、进程控制、内存管理等)，都必须通过系统调用方式向操作系统提出服务请求，并由操作系统代为完成。</p>
<p>这些系统调用按功能大致可分为如下几类：</p>
<ul>
<li>设备管理。完成设备的请求或释放，以及设备启动等功能。</li>
<li>文件管理。完成文件的读、写、创建及删除等功能。</li>
<li>进程控制。完成进程的创建、撤销、阻塞及唤醒等功能。</li>
<li>进程通信。完成进程之间的消息传递或信号传递等功能。</li>
<li>内存管理。完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。</li>
</ul>
<hr>
<h4 id="进程和线程"><a href="#进程和线程" class="headerlink" title="进程和线程"></a>进程和线程</h4><h5 id="进程和线程的区别"><a href="#进程和线程的区别" class="headerlink" title="进程和线程的区别"></a>进程和线程的区别</h5><p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/jvm/java-runtime-data-areas-jdk1.8.png" alt="Java 运行时数据区域（JDK1.8 之后）"></p>
<p>一个进程中可以有多个线程，多个线程共享进程的<strong>堆</strong>和<strong>方法区 (JDK1.8 之后的元空间)**资源，但是每个线程有自己的**程序计数器**、</strong>虚拟机栈** 和 <strong>本地方法栈</strong>。</p>
<p><strong>总结：</strong> 线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。线程和进程最大的不同在于基本上各进程是独立的，而各线程则不一定，因为同一进程中的线程极有可能会相互影响。线程执行开销小，但不利于资源的管理和保护；而进程正相反。</p>
<hr>
<h5 id="进程有哪几种状态"><a href="#进程有哪几种状态" class="headerlink" title="进程有哪几种状态?"></a>进程有哪几种状态?</h5><p>一般把进程大致分为 5 种状态，这一点和<a target="_blank" rel="noopener" href="https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md#6-%E8%AF%B4%E8%AF%B4%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E5%92%8C%E7%8A%B6%E6%80%81">线程open in new window</a>很像！</p>
<ul>
<li><strong>创建状态(new)</strong> ：进程正在被创建，尚未到就绪状态。</li>
<li><strong>就绪状态(ready)</strong> ：进程已处于准备运行状态，即进程获得了除了处理器之外的一切所需资源，一旦得到处理器资源(处理器分配的时间片)即可运行。</li>
<li><strong>运行状态(running)</strong> ：进程正在处理器上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。</li>
<li><strong>阻塞状态(waiting)</strong> ：又称为等待状态，进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲，该进程也不能运行。</li>
<li><strong>结束状态(terminated)</strong> ：进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。</li>
</ul>
<hr>
<h5 id="进程间的通信方式"><a href="#进程间的通信方式" class="headerlink" title="进程间的通信方式"></a>进程间的通信方式</h5><ol>
<li><strong>管道&#x2F;匿名管道(Pipes)</strong> ：用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。</li>
<li><strong>有名管道(Named Pipes)</strong> : 匿名管道由于没有名字，只能用于亲缘关系的进程间通信。为了克服这个缺点，提出了有名管道。有名管道严格遵循**先进先出(first in first out)**。有名管道&#x3D;&#x3D;以磁盘文件的方式存在&#x3D;&#x3D;，可以实现本机任意两个进程通信。</li>
<li><strong>信号(Signal)</strong> ：信号是一种比较复杂的通信方式，用于通知接收进程某个事件已经发生；</li>
<li><strong>消息队列(Message Queuing)</strong> ：消息队列是消息的链表,具有特定的格式,&#x3D;&#x3D;存放在内存中&#x3D;&#x3D;并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道（匿名管道：只存在于内存中的文件；有名管道：存在于实际的磁盘介质或者文件系统）不同的是&#x3D;&#x3D;消息队列存放在内核&#x3D;&#x3D;中，只有在内核重启(即，操作系统重启)或者显式地删除一个消息队列时，该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。<strong>消息队列克服了信号承载信息量少，管道只能承载无格式字 节流以及缓冲区大小受限等缺点。</strong></li>
<li><strong>信号量(Semaphores)</strong> ：信号量是一个计数器，用于多进程对共享数据的访问，信号量的意图在于进程间同步。&#x3D;&#x3D;这种通信方式主要用于解决与同步相关的问题并避免竞争条件。&#x3D;&#x3D;</li>
<li><strong>共享内存(Shared memory)</strong> ：使得多个进程可以访问同一块内存空间，不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作，如<strong>互斥锁和信号量</strong>等。可以说这是最有用的<strong>进程间通信方式</strong>。</li>
<li><strong>套接字(Sockets)</strong> : 此方法主要用于在<strong>客户端和服务器之间通过网络进行通信</strong>。套接字是支持 TCP&#x2F;IP 的网络通信的基本操作单元，可以看做是不同主机之间的进程进行双向通信的端点，简单的说就是通信的两方的一种约定，用套接字中的相关函数来完成通信过程。</li>
</ol>
<hr>
<h5 id="x3D-x3D-线程间的同步的方式-x3D-x3D"><a href="#x3D-x3D-线程间的同步的方式-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;线程间的同步的方式&#x3D;&#x3D;"></a>&#x3D;&#x3D;线程间的同步的方式&#x3D;&#x3D;</h5><p>线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。操作系统一般有下面三种线程同步的方式：</p>
<ol>
<li>**互斥量(Mutex)**：采用互斥对象机制，只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个，所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。</li>
<li><strong>信号量(Semaphore)</strong> ：它允许同一时刻多个线程访问同一资源，但是需要控制同一时刻访问此资源的最大线程数量。</li>
<li><strong>事件(Event)</strong> :Wait&#x2F;Notify：通过通知操作的方式来保持多线程同步，还可以方便的实现多线程优先级的比较操作。</li>
</ol>
<hr>
<h5 id="进程的调度算法"><a href="#进程的调度算法" class="headerlink" title="进程的调度算法"></a>进程的调度算法</h5><ol>
<li><strong>先到先服务(FCFS)调度算法</strong> : 从就绪队列中选择一个最先进入该队列的进程为之分配资源，使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。</li>
<li><strong>短作业优先(SJF)的调度算法</strong> : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源，使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。</li>
<li><strong>时间片轮转调度算法</strong> : 时间片轮转调度是一种最古老，最简单，最公平且使用最广的算法，又称 RR(Round robin)调度。每个进程被分配一个时间段，称作它的时间片，即该进程允许运行的时间。</li>
<li><strong>多级反馈队列调度算法</strong> ：前面介绍的几种进程调度的算法都有一定的局限性。如<strong>短进程优先的调度算法，仅照顾了短进程而忽略了长进程</strong> 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业（进程）迅速完成。，因而它是目前<strong>被公认的一种较好的进程调度算法</strong>，UNIX 操作系统采取的便是这种调度算法。</li>
<li><strong>优先级调度</strong> ： 为每个流程分配优先级，首先执行具有最高优先级的进程，依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求，时间要求或任何其他资源要求来确定优先级。</li>
</ol>
<hr>
<h4 id="操作系统内存管理基础"><a href="#操作系统内存管理基础" class="headerlink" title="操作系统内存管理基础"></a>操作系统内存管理基础</h4><h5 id="内存管理介绍"><a href="#内存管理介绍" class="headerlink" title="内存管理介绍"></a>内存管理介绍</h5><p>操作系统的内存管理主要负责内存的分配与回收（malloc 函数：申请内存，free 函数：释放内存），另外地址转换也就是将逻辑地址转换成相应的物理地址等功能也是操作系统内存管理做的事情。</p>
<h5 id="常见的几种内存管理机制"><a href="#常见的几种内存管理机制" class="headerlink" title="常见的几种内存管理机制"></a>常见的几种内存管理机制</h5><p>简单分为<strong>连续分配管理方式</strong>和<strong>非连续分配管理方式</strong>这两种。连续分配管理方式是指为一个用户程序分配一个连续的内存空间，常见的如 <strong>块式管理</strong> 。同样地，非连续分配管理方式允许一个程序使用的内存分布在离散或者说不相邻的内存中，常见的如<strong>页式管理</strong> 和 <strong>段式管理</strong>。</p>
<ul>
<li><strong>块式管理</strong> ： 远古时代的计算机操作系统的内存管理方式。将内存分为几个固定大小的块，每个块中只包含一个进程。如果程序运行需要内存的话，操作系统就分配给它一块，如果程序运行只需要很小的空间的话，分配的这块内存很大一部分几乎被浪费了。这些在每个块中未被利用的空间，我们称之为碎片。</li>
<li><strong>页式管理</strong> ：把主存分为大小相等且固定的一页一页的形式，页较小，相比于块式管理的划分粒度更小，提高了内存利用率，减少了碎片。页式管理通过页表对应逻辑地址和物理地址。</li>
<li><strong>段式管理</strong> ： 页式管理虽然提高了内存利用率，但是页式管理其中的页并无任何实际意义。 段式管理把主存分为一段段的，段是有实际意义的，每个段定义了一组逻辑信息，例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址。</li>
<li><strong>段页式管理机制</strong> 。段页式管理机制结合了段式管理和页式管理的优点。简单来说段页式管理机制就是把主存先分成若干段，每个段又分成若干页，也就是说 <strong>段页式管理机制</strong> 中段与段之间以及段的内部的都是离散的</li>
</ul>
<p>简单来说：页是物理单位，段是逻辑单位。分页可以有效提高内存利用率，分段可以更好满足用户需求。</p>
<hr>
<h5 id="x3D-x3D-快表和多级页表-x3D-x3D"><a href="#x3D-x3D-快表和多级页表-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;快表和多级页表&#x3D;&#x3D;"></a>&#x3D;&#x3D;快表和多级页表&#x3D;&#x3D;</h5><p>在分页内存管理中，很重要的两点是：</p>
<ol>
<li>虚拟地址到物理地址的转换要快。</li>
<li>解决虚拟地址空间大，页表也会很大的问题。</li>
</ol>
<h6 id="快表"><a href="#快表" class="headerlink" title="快表"></a>快表</h6><p>为了提高虚拟地址到物理地址的转换速度，操作系统在 <strong>页表方案</strong> 基础之上引入了 <strong>快表</strong> 来加速虚拟地址到物理地址的转换。我们可以把快表理解为一种特殊的高速缓冲存储器（Cache），其中的内容是页表的一部分或者全部内容。作为页表的 Cache，它的作用与页表相似，但是提高了访问速率。由于采用页表做地址转换，读写内存数据时 CPU 要访问两次主存。有了快表，有时只要访问一次高速缓冲存储器，一次主存，这样可加速查找并提高指令执行速度。</p>
<p>使用快表之后的地址转换流程是这样的：</p>
<ol>
<li>根据虚拟地址中的页号查快表；</li>
<li>如果该页在快表中，直接从快表中读取相应的物理地址；</li>
<li>如果该页不在快表中，就访问内存中的页表，再从页表中得到物理地址，同时将页表中的该映射表项添加到快表中；</li>
<li>当快表填满后，又要登记新页时，就按照一定的淘汰策略淘汰掉快表中的一个页。</li>
</ol>
<h6 id="多级页表"><a href="#多级页表" class="headerlink" title="多级页表"></a>多级页表</h6><p>引入多级页表的主要目的是为了避免把全部页表一直放在内存中占用过多空间，特别是那些根本就不需要的页表就不需要保留在内存中。</p>
<p>多级页表属于时间换空间的典型场景。</p>
<h6 id="总结-5"><a href="#总结-5" class="headerlink" title="总结"></a>总结</h6><p>为了提高内存的空间性能，提出了多级页表的概念；但是提到空间性能是以浪费时间性能为基础的，因此为了补充损失的时间性能，提出了快表（即 TLB）的概念。 不论是快表还是多级页表实际上都利用到了程序的&#x3D;&#x3D;局部性&#x3D;&#x3D;原理</p>
<hr>
<h5 id="分页机制和分段机制的共同点和区别"><a href="#分页机制和分段机制的共同点和区别" class="headerlink" title="分页机制和分段机制的共同点和区别"></a>分页机制和分段机制的共同点和区别</h5><p><strong>共同点</strong> ： </p>
<ul>
<li>分页机制和分段机制都是为了提高内存利用率，减少内存碎片。</li>
<li>页和段都是离散存储的，所以两者都是离散分配内存的方式。但是，每个页和段中的内存是连续的。</li>
</ul>
<p><strong>区别</strong> ： </p>
<ul>
<li>页的大小是固定的，由操作系统决定；而段的大小不固定，取决于我们当前运行的程序。</li>
<li>分页仅仅是为了满足操作系统内存管理的需求，而段是逻辑信息的单位，在程序中可以体现为代码段，数据段，能够更好满足用户的需要。</li>
</ul>
<hr>
<h5 id="逻辑-虚拟-地址和物理地址"><a href="#逻辑-虚拟-地址和物理地址" class="headerlink" title="逻辑(虚拟)地址和物理地址"></a>逻辑(虚拟)地址和物理地址</h5><p>指针里面存储的数值就可以理解成为内存里的一个地址，这个地址也就是我们说的逻辑地址，逻辑地址由操作系统决定。物理地址指的是真实物理内存中地址，更具体一点来说就是内存地址寄存器中的地址。物理地址是内存单元真正的地址。</p>
<hr>
<h5 id="CPU-寻址了解吗-为什么需要虚拟地址空间"><a href="#CPU-寻址了解吗-为什么需要虚拟地址空间" class="headerlink" title="CPU 寻址了解吗?为什么需要虚拟地址空间?"></a>CPU 寻址了解吗?为什么需要虚拟地址空间?</h5><p>现代处理器使用的是一种称为 <strong>虚拟寻址(Virtual Addressing)</strong> 的寻址方式。<strong>使用虚拟寻址，CPU 需要将虚拟地址翻译成物理地址，这样才能访问到真实的物理内存。</strong> 实际上完成虚拟地址转换为物理地址的硬件是 CPU 中含有一个被称为 <strong>内存管理单元（Memory Management Unit, MMU）</strong> 的硬件。</p>
<p><strong>为什么要有虚拟地址空间呢？</strong></p>
<p>没有虚拟地址空间的时候，<strong>程序直接访问和操作的都是物理内存</strong> 。但是这样有什么问题呢？</p>
<ol>
<li><strong>用户程序可以访问任意内存</strong>，寻址内存的每个字节，这样就很容易（有意或者无意）破坏操作系统，造成操作系统崩溃。</li>
<li>想要<strong>同时运行多个程序特别困难</strong>，比如你想同时运行一个微信和一个 QQ 音乐都不行。为什么呢？举个简单的例子：微信在运行的时候给内存地址 1xxx 赋值后，QQ 音乐也同样给内存地址 1xxx 赋值，那么 QQ 音乐对内存的赋值就会覆盖微信之前所赋的值，这就造成微信这个程序会崩溃。</li>
</ol>
<p><strong>总结来说：如果直接把物理地址暴露出来的话会带来严重问题，比如可能对操作系统造成伤害以及给同时运行多个程序造成困难。</strong></p>
<p>通过虚拟地址访问内存有以下优势：</p>
<ul>
<li>程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。</li>
<li>程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时，内存管理器会将物理内存页（通常大小为 4 KB）保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。</li>
<li>不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。</li>
</ul>
<hr>
<h5 id="虚拟内存"><a href="#虚拟内存" class="headerlink" title="虚拟内存"></a>虚拟内存</h5><h6 id="什么是虚拟内存-Virtual-Memory"><a href="#什么是虚拟内存-Virtual-Memory" class="headerlink" title="什么是虚拟内存(Virtual Memory)?"></a>什么是虚拟内存(Virtual Memory)?</h6><p>很多时候我们使用了很多占内存的软件，这些软件占用的内存可能已经远远超出了我们电脑本身具有的物理内存。<strong>为什么可以这样呢？</strong> 正是因为 <strong>虚拟内存</strong> 的存在，通过 <strong>虚拟内存</strong> 可以让程序拥有超过系统物理内存大小的可用内存空间。另外，<strong>虚拟内存为每个进程提供了一个一致的、私有的地址空间，它让每个进程产生了一种自己在独享主存的错觉（每个进程拥有一片连续完整的内存空间）</strong>。这样会更加有效地管理内存并减少出错。</p>
<p><strong>虚拟内存</strong>是计算机系统内存管理的一种技术，我们可以手动设置自己电脑的虚拟内存。不要单纯认为虚拟内存只是“使用硬盘空间来扩展内存“的技术。<strong>虚拟内存的重要意义是它定义了一个连续的虚拟地址空间</strong>，并且 <strong>把内存扩展到硬盘空间</strong>。</p>
<hr>
<h6 id="局部性原理"><a href="#局部性原理" class="headerlink" title="局部性原理"></a>局部性原理</h6><p>局部性原理是虚拟内存技术的基础，正是因为程序运行具有局部性原理，才可以只装入部分程序到内存就开始运行。</p>
<p>局部性原理表现在以下两个方面：</p>
<ol>
<li><strong>时间局部性</strong> ：如果程序中的某条指令一旦执行，不久以后该指令可能再次执行；如果某数据被访问过，不久以后该数据可能再次被访问。产生时间局部性的典型原因，是由于在程序中存在着大量的循环操作。</li>
<li><strong>空间局部性</strong> ：一旦程序访问了某个存储单元，在不久之后，其附近的存储单元也将被访问，即程序在一段时间内所访问的地址，可能集中在一定的范围之内，这是因为指令通常是顺序存放、顺序执行的，数据也一般是以向量、数组、表等形式簇聚存储的。</li>
</ol>
<hr>
<h6 id="虚拟存储器"><a href="#虚拟存储器" class="headerlink" title="虚拟存储器"></a>虚拟存储器</h6><p>基于局部性原理，在程序装入时，可以将程序的一部分装入内存，而将其他部分留在外存，就可以启动程序执行。由于外存往往比内存大很多，所以我们运行的软件的内存大小实际上是可以比计算机系统实际的内存大小大的。在程序执行过程中，当所访问的信息不在内存时，由操作系统将所需要的部分调入内存，然后继续执行程序。另一方面，操作系统将内存中暂时不使用的内容换到外存上，从而腾出空间存放将要调入内存的信息。这样，计算机好像为用户提供了一个比实际内存大得多的存储器——<strong>虚拟存储器</strong>。</p>
<p>实际上，我觉得虚拟内存同样是一种时间换空间的策略，你用 CPU 的计算时间，页的调入调出花费的时间，换来了一个虚拟的更大的空间来支持程序的运行。不得不感叹，程序世界几乎不是时间换空间就是空间换时间。</p>
<hr>
<h6 id="虚拟内存的技术实现"><a href="#虚拟内存的技术实现" class="headerlink" title="虚拟内存的技术实现"></a>虚拟内存的技术实现</h6><p><strong>虚拟内存的实现需要建立在离散分配的内存管理方式的基础上。</strong> 虚拟内存的实现有以下三种方式：</p>
<ol>
<li><strong>请求分页存储管理</strong> ：建立在分页管理之上，为了支持虚拟存储器功能而增加了请求调页功能和页面置换功能。请求分页是目前最常用的一种实现虚拟存储器的方法。请求分页存储管理系统中，在作业开始运行之前，仅装入当前要执行的部分段即可运行。假如在作业运行的过程中发现要访问的页面不在内存，则由处理器通知操作系统按照对应的页面置换算法将相应的页面调入到主存，同时操作系统也可以将暂时不用的页面置换到外存中。</li>
<li><strong>请求分段存储管理</strong> ：建立在分段存储管理之上，增加了请求调段功能、分段置换功能。请求分段储存管理方式就如同请求分页储存管理方式一样，在作业开始运行之前，仅装入当前要执行的部分段即可运行；在执行过程中，可使用请求调入中断动态装入要访问但又不在内存的程序段；当内存空间已满，而又需要装入新的段时，根据置换功能适当调出某个段，以便腾出空间而装入新的段。</li>
<li><strong>请求段页式存储管理</strong></li>
</ol>
<p><strong>这里多说一下？很多人容易搞混请求分页与分页存储管理，两者有何不同呢？</strong></p>
<p>请求分页存储管理建立在分页管理之上。他们的根本区别是是否将程序所需的全部地址空间都装入主存，这也是请求分页存储管理可以提供虚拟内存的原因，我们在上面已经分析过了。</p>
<p>它们之间的根本区别在于是否将一作业的全部地址空间同时装入主存。请求分页存储管理不要求将作业全部地址空间同时装入主存。基于这一点，请求分页存储管理可以提供虚存，而分页存储管理却不能提供虚存。</p>
<p>不管是上面那种实现方式，我们一般都需要：</p>
<ol>
<li>一定容量的内存和外存：在载入程序的时候，只需要将程序的一部分装入内存，而将其他部分留在外存，然后程序就可以执行了；</li>
<li><strong>缺页中断</strong>：如果<strong>需执行的指令或访问的数据尚未在内存</strong>（称为缺页或缺段），则由处理器通知操作系统将相应的页面或段<strong>调入到内存</strong>，然后继续执行程序；</li>
<li><strong>虚拟地址空间</strong> ：逻辑地址到物理地址的变换。</li>
</ol>
<hr>
<h6 id="页面置换算法"><a href="#页面置换算法" class="headerlink" title="页面置换算法"></a>页面置换算法</h6><p>地址映射过程中，若在页面中发现所要访问的页面不在内存中，则发生缺页中断 。</p>
<p>当发生&#x3D;&#x3D;缺页中断&#x3D;&#x3D;时，如果当前内存中并没有空闲的页面，操作系统就必须在内存选择一个页面将其移出内存，以便为即将调入的页面让出空间。用来选择淘汰哪一页的规则叫做页面置换算法，我们可以把页面置换算法看成是淘汰页面的规则。</p>
<ul>
<li><strong>OPT 页面置换算法（最佳页面置换算法）</strong> ：最佳(Optimal, OPT)置换算法所选择的被淘汰页面将是以后永不使用的，或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若千页面中哪个是未来最长时间内不再被访问的，因而该算法无法实现。一般作为衡量其他置换算法的方法。</li>
<li><strong>FIFO（First In First Out） 页面置换算法（先进先出页面置换算法）</strong> : 总是淘汰最先进入内存的页面，即选择在内存中驻留时间最久的页面进行淘汰。</li>
<li><strong>LRU （Least Recently Used）页面置换算法（最近最久未使用页面置换算法）</strong> ：LRU 算法赋予每个页面一个访问字段，用来记录一个页面自上次被访问以来所经历的时间 T，当须淘汰一个页面时，选择现有页面中其 T 值最大的，即最近最久未使用的页面予以淘汰。</li>
<li><strong>LFU （Least Frequently Used）页面置换算法（最少使用页面置换算法）</strong> : 该置换算法选择在之前时期使用最少的页面作为淘汰页。</li>
</ul>
<hr>
<h3 id="什么是死锁"><a href="#什么是死锁" class="headerlink" title="什么是死锁"></a>什么是死锁</h3><p>死锁描述的是这样一种情况：多个进程&#x2F;线程同时被阻塞，它们中的一个或者全部都在等待某个资源被释放。由于进程&#x2F;线程被无限期地阻塞，因此程序不可能正常终止。</p>
<hr>
<h4 id="死锁的四个必要条件-产生死锁的四个必要条件是什么"><a href="#死锁的四个必要条件-产生死锁的四个必要条件是什么" class="headerlink" title="死锁的四个必要条件(产生死锁的四个必要条件是什么?)"></a>死锁的四个必要条件(<strong>产生死锁的四个必要条件是什么?</strong>)</h4><ul>
<li><strong>互斥</strong>：资源必须处于非共享模式，即一次只有一个进程可以使用。如果另一进程申请该资源，那么必须等待直到该资源被释放为止。</li>
<li><strong>占有并等待</strong>：一个进程至少应该占有一个资源，并等待另一资源，而该资源被其他进程所占有。</li>
<li><strong>非抢占</strong>：资源不能被抢占。只能在持有资源的进程完成任务后，该资源才会被释放。</li>
<li><strong>循环等待</strong>：有一组等待进程 <code>&#123;P0, P1,..., Pn&#125;</code>， <code>P0</code> 等待的资源被 <code>P1</code> 占有，<code>P1</code> 等待的资源被 <code>P2</code> 占有，……，<code>Pn-1</code> 等待的资源被 <code>Pn</code> 占有，<code>Pn</code> 等待的资源被 <code>P0</code> 占有。</li>
</ul>
<p><strong>注意 ⚠️</strong> ：这四个条件是产生死锁的 <strong>必要条件</strong> ，也就是说只要系统发生死锁，这些条件必然成立，而只要上述条件之一不满足，就不会发生死锁。</p>
<hr>
<h4 id="x3D-x3D-解决死锁的方法-x3D-x3D"><a href="#x3D-x3D-解决死锁的方法-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;解决死锁的方法&#x3D;&#x3D;"></a>&#x3D;&#x3D;解决死锁的方法&#x3D;&#x3D;</h4><ul>
<li><strong>预防</strong> 是采用某种策略，<strong>限制并发进程对资源的请求</strong>，从而使得死锁的必要条件在系统执行的任何时间上都不满足。</li>
<li><strong>避免</strong>则是系统在分配资源时，根据资源的使用情况<strong>提前做出预测</strong>，从而<strong>避免死锁的发生</strong></li>
<li><strong>检测</strong>是指系统设有<strong>专门的机构</strong>，当死锁发生时，该机构能够检测死锁的发生，并精确地确定与死锁有关的进程和资源。</li>
<li><strong>解除</strong> 是与检测相配套的一种措施，用于<strong>将进程从死锁状态下解脱出来</strong>。</li>
</ul>
<h5 id="死锁的预防"><a href="#死锁的预防" class="headerlink" title="死锁的预防"></a>死锁的预防</h5><p>破坏第一个条件 <strong>互斥条件</strong>：使得资源是可以同时访问的，这是种简单的方法，磁盘就可以用这种方法管理，但是我们要知道，有很多资源 <strong>往往是不能同时访问的</strong> ，所以这种做法在大多数的场合是行不通的。</p>
<p>破坏第三个条件 <strong>非抢占</strong> ：也就是说可以采用 <strong>剥夺式调度算法</strong>，但剥夺式调度方法目前一般仅适用于 <strong>主存资源</strong> 和 <strong>处理器资源</strong> 的分配，并不适用于所有的资源，会导致 <strong>资源利用率下降</strong>。</p>
<p>所以一般比较实用的 <strong>预防死锁的方法</strong>，是通过考虑破坏第二个条件和第四个条件。</p>
<p><strong>1、静态分配策略</strong></p>
<p>静态分配策略可以破坏死锁产生的第二个条件（&#x3D;&#x3D;占有并等待&#x3D;&#x3D;）。所谓静态分配策略，就是指一个进程必须在执行前就申请到它所需要的全部资源，并且直到它所要的资源都得到满足之后才开始执行。进程要么占有所有的资源然后开始执行，要么不占有资源，不会出现占有一些资源等待一些资源的情况。</p>
<p>静态分配策略逻辑简单，实现也很容易，但这种策略 <strong>严重地降低了资源利用率</strong>，因为在每个进程所占有的资源中，有些资源是在比较靠后的执行时间里采用的，甚至有些资源是在额外的情况下才使用的，这样就可能造成一个进程占有了一些 <strong>几乎不用的资源而使其他需要该资源的进程产生等待</strong> 的情况。</p>
<p><strong>2、层次分配策略</strong></p>
<p>层次分配策略破坏了产生死锁的第四个条件(&#x3D;&#x3D;循环等待&#x3D;&#x3D;)。在层次分配策略下，所有的资源被分成了多个层次，一个进程得到某一次的一个资源后，它只能再申请较高一层的资源；当一个进程要释放某层的一个资源时，必须先释放所占用的较高层的资源，按这种策略，是不可能出现循环等待链的，因为那样的话，就出现了已经申请了较高层的资源，反而去申请了较低层的资源，不符合层次分配策略（进程A占据a，申请b，进程B占据b，申请a，因为只能申请高层资源，A申请b可得b高于a，B申请a可得a高于b，悖论）。</p>
<hr>
<h5 id="死锁的避免"><a href="#死锁的避免" class="headerlink" title="死锁的避免"></a>死锁的避免</h5><p> <strong>破坏</strong> 死锁产生的四个必要条件之一就可以成功 <strong>预防系统发生死锁</strong> ，但是会导致 <strong>低效的进程运行</strong> 和 <strong>资源使用率</strong> 。死锁的避免相反，它的角度是允许系统中<strong>同时存在四个必要条件</strong> ，只要掌握并发进程中与每个进程有关的资源动态申请情况，做出 <strong>明智和合理的选择</strong> ，仍然可以避免死锁，因为四大条件仅仅是产生死锁的必要条件。</p>
<p>将系统的状态分为 <strong>安全状态</strong> 和 <strong>不安全状态</strong> ，每当在未申请者分配资源前先测试系统状态，若把系统资源分配给申请者会产生死锁，则拒绝分配，否则接受申请，并为它分配资源。</p>
<p>那么如何保证系统保持在安全状态呢？通过算法，其中最具有代表性的 <strong>避免死锁算法</strong> 就是 Dijkstra 的银行家算法，银行家算法用一句话表达就是：当一个进程申请使用资源的时候，<strong>银行家算法</strong> 通过先 <strong>试探</strong> 分配给该进程资源，然后通过 <strong>安全性算法</strong> 判断分配后系统是否处于安全状态，若不安全则试探分配作废，让该进程继续等待，若能够进入到安全的状态，则就 <strong>真的分配资源给该进程</strong>。</p>
<p><strong>银行家算法</strong></p>
<p>判断当前系统资源能否让某一进程执行完毕释放资源，不能则当前系统处于不安全状态，若能分配资源给该进程并当进程执行完毕后回收进程资源。继续执行该算法。若所有进程都可执行完毕，系统处于安全状态</p>
<p><a target="_blank" rel="noopener" href="https://blog.csdn.net/qq_33414271/article/details/80245715">https://blog.csdn.net/qq_33414271/article/details/80245715</a></p>
<hr>
<h5 id="死锁的检测"><a href="#死锁的检测" class="headerlink" title="死锁的检测"></a>死锁的检测</h5><p>对资源的分配加以限制可以 <strong>预防和避免</strong> 死锁的发生，但是都不利于各进程对系统资源的<strong>充分共享</strong>。解决死锁问题的另一条途径是 <strong>死锁检测和解除</strong> (这里突然联想到了&#x3D;&#x3D;乐观锁和悲观锁&#x3D;&#x3D;，感觉死锁的检测和解除就像是 <strong>乐观锁</strong> ，分配资源时不去提前管会不会发生死锁了，等到真的死锁出现了再来解决嘛，而 <strong>死锁的预防和避免</strong> 更像是悲观锁，总是觉得死锁会出现，所以在分配资源的时候就很谨慎)。系统 <strong>定时地运行一个 “死锁检测”</strong> 的程序，判断系统内是否出现死锁，如果检测到系统发生了死锁，再采取措施去解除它。</p>
<hr>
<h6 id="进程-资源分配图"><a href="#进程-资源分配图" class="headerlink" title="进程-资源分配图"></a>进程-资源分配图</h6><p>操作系统中的每一刻时刻的<strong>系统状态</strong>都可以用<strong>进程-资源分配图</strong>来表示，进程-资源分配图是描述进程和资源申请及分配关系的一种有向图，可用于<strong>检测系统是否处于死锁状态</strong>。</p>
<hr>
<h6 id="死锁检测步骤"><a href="#死锁检测步骤" class="headerlink" title="死锁检测步骤"></a>死锁检测步骤</h6><ol>
<li>如果进程-资源分配图中无环路，则此时系统没有发生死锁</li>
<li>如果进程-资源分配图中有环路，且每个资源类仅有一个资源，则系统中已经发生了死锁。</li>
<li>如果进程-资源分配图中有环路，且涉及到的资源类有多个资源，此时系统未必会发生死锁。如果能在进程-资源分配图中找出一个 <strong>既不阻塞又非独立的进程</strong> ，该进程能够在有限的时间内归还占有的资源，也就是把边给消除掉了，重复此过程，直到能在有限的时间内 <strong>消除所有的边</strong> ，则不会发生死锁，否则会发生死锁。(消除边的过程类似于 <strong>拓扑排序</strong>)</li>
</ol>
<hr>
<h3 id="Linux-基础知识总结"><a href="#Linux-基础知识总结" class="headerlink" title="Linux 基础知识总结"></a>Linux 基础知识总结</h3><h4 id="从认识操作系统开始"><a href="#从认识操作系统开始" class="headerlink" title="从认识操作系统开始"></a>从认识操作系统开始</h4><h5 id="操作系统简介"><a href="#操作系统简介" class="headerlink" title="操作系统简介"></a>操作系统简介</h5><h5 id="操作系统简单分类"><a href="#操作系统简单分类" class="headerlink" title="操作系统简单分类"></a>操作系统简单分类</h5><h6 id="Windows"><a href="#Windows" class="headerlink" title="Windows"></a>Windows</h6><h6 id="Unix"><a href="#Unix" class="headerlink" title="Unix"></a>Unix</h6><h6 id="Linux"><a href="#Linux" class="headerlink" title="Linux"></a>Linux</h6><p><strong>Linux 是一套免费使用、开源的类 Unix 操作系统。</strong> Linux 存在着许多不同的发行版本，但它们都使用了 <strong>Linux 内核</strong> 。</p>
<h6 id="Mac-OS"><a href="#Mac-OS" class="headerlink" title="Mac OS"></a>Mac OS</h6><h5 id="操作系统的内核（Kernel）"><a href="#操作系统的内核（Kernel）" class="headerlink" title="操作系统的内核（Kernel）"></a>操作系统的内核（Kernel）</h5><p><strong>内核</strong>（英语：Kernel，又称核心）在计算机科学中是一个用来管理软件发出的数据 I&#x2F;O（输入与输出）要求的电脑程序，将这些要求转译为数据处理的指令并交由中央处理器（CPU）及电脑中其他电子组件进行处理，是现代操作系统中最基本的部分。它是为众多应用程序提供对计算机硬件的安全访问的一部分软件，这种访问是有限的，并由内核决定一个程序在什么时候对某部分硬件操作多长时间。 <strong>直接对硬件操作是非常复杂的。所以内核通常提供一种硬件抽象的方法，来完成这些操作。有了这个，通过进程间通信机制及系统调用，应用进程可间接控制所需的硬件资源（特别是处理器及 IO 设备）。</strong></p>
<p>简单概括两点：</p>
<ol>
<li><strong>操作系统的内核（Kernel）是操作系统的核心部分，它负责系统的内存管理，硬件设备的管理，文件系统的管理以及应用程序的管理。</strong></li>
<li><strong>操作系统的内核是连接应用程序和硬件的桥梁，决定着操作系统的性能和稳定性。</strong></li>
</ol>
<hr>
<h5 id="中央处理器（CPU，Central-Processing-Unit）"><a href="#中央处理器（CPU，Central-Processing-Unit）" class="headerlink" title="中央处理器（CPU，Central Processing Unit）"></a>中央处理器（CPU，Central Processing Unit）</h5><p>关于 CPU 简单概括三点：</p>
<ol>
<li><strong>CPU 是一台计算机的运算核心（Core）+控制核心（ Control Unit），可以称得上是计算机的大脑。</strong></li>
<li><strong>CPU 主要包括两个部分：控制器+运算器。</strong></li>
<li><strong>CPU 的根本任务就是执行指令，对计算机来说最终都是一串由“0”和“1”组成的序列。</strong></li>
</ol>
<hr>
<h5 id="CPU-vs-Kernel-内核"><a href="#CPU-vs-Kernel-内核" class="headerlink" title="CPU vs Kernel(内核)"></a>CPU vs Kernel(内核)</h5><ol>
<li>操作系统的内核（Kernel）属于操作系统层面，而 CPU 属于硬件。</li>
<li>CPU 主要提供运算，处理各种指令的能力。内核（Kernel）主要负责系统管理比如内存管理，它屏蔽了对硬件的操作。<br><img src="E:\XxdBlog\source_posts\images\test\image-20230302151547639.png" alt="image-20230302151547639"></li>
</ol>
<hr>
<h5 id="系统调用-1"><a href="#系统调用-1" class="headerlink" title="系统调用"></a>系统调用</h5><p>根据进程访问资源的特点，我们可以把进程在系统上的运行分为两个级别：</p>
<ol>
<li><strong>用户态(user mode)</strong> : 用户态运行的进程或可以直接读取用户数据的程序。</li>
<li><strong>系统态(kernel mode)</strong>: 可以简单的理解系统态运行的进程或程序几乎可以访问计算机的任何资源，不受限制。</li>
</ol>
<p><strong>说了用户态和系统态之后，那么什么是系统调用呢？</strong></p>
<p>我们运行的程序基本都是运行在用户态，如果我们调用操作系统提供的系统态级别的子功能咋办呢？那就需要系统调用了！</p>
<p>也就是说在我们运行的用户程序中，凡是与系统态级别的资源有关的操作（如文件管理、进程控制、内存管理等)，都必须通过系统调用方式向操作系统提出服务请求，并由操作系统代为完成。</p>
<p>这些系统调用按功能大致可分为如下几类：</p>
<ul>
<li><strong>设备管理</strong> ：完成设备的请求或释放，以及设备启动等功能。</li>
<li><strong>文件管理</strong> ：完成文件的读、写、创建及删除等功能。</li>
<li><strong>进程控制</strong> ：完成进程的创建、撤销、阻塞及唤醒等功能。</li>
<li><strong>进程通信</strong> ：完成进程之间的消息传递或信号传递等功能。</li>
<li><strong>内存管理</strong> ：完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。</li>
</ul>
<p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/L181kk2Eou-compress.jpg" alt="img"></p>
<hr>
<h4 id="初探-Linux"><a href="#初探-Linux" class="headerlink" title="初探 Linux"></a>初探 Linux</h4><h5 id="Linux-简介"><a href="#Linux-简介" class="headerlink" title="Linux 简介"></a>Linux 简介</h5><ol>
<li><strong>类 Unix 系统</strong> ： Linux 是一种自由、开放源码的类似 Unix 的操作系统</li>
<li><strong>Linux 本质是指 Linux 内核</strong> ： 严格来讲，Linux 这个词本身只表示 Linux 内核，单独的 Linux 内核并不能成为一个可以正常工作的操作系统。所以，就有了各种 Linux 发行版。</li>
</ol>
<hr>
<h4 id="Linux-文件系统概览"><a href="#Linux-文件系统概览" class="headerlink" title="Linux 文件系统概览"></a>Linux 文件系统概览</h4><h5 id="Linux-文件系统简介"><a href="#Linux-文件系统简介" class="headerlink" title="Linux 文件系统简介"></a>Linux 文件系统简介</h5><p><strong>在 Linux 操作系统中，所有被操作系统管理的资源，例如网络接口卡、磁盘驱动器、打印机、输入输出设备、普通文件或是目录都被看作是一个文件。</strong> 也就是说在 Linux 系统中有一个重要的概念：<strong>一切都是文件</strong>。</p>
<hr>
<h5 id="inode-介绍"><a href="#inode-介绍" class="headerlink" title="inode 介绍"></a>inode 介绍</h5><p><strong>inode 是 linux&#x2F;unix 文件系统的基础。那么，inode 是什么?有什么作用呢?</strong></p>
<p>硬盘的最小存储单位是扇区(Sector)，块(block)由多个扇区组成。文件数据存储在块中。块的最常见的大小是 4kb，约为 8 个连续的扇区组成（每个扇区存储 512 字节）。一个文件可能会占用多个 block，但是一个块只能存放一个文件。</p>
<p>虽然，我们将文件存储在了块(block)中，但是我们还需要一个空间来存储文件的 <strong>元信息 metadata</strong> ：如某个文件被分成几块、每一块在的地址、文件拥有者，创建时间，权限，大小等。这种 <strong>存储文件元信息的区域就叫 inode</strong>，译为索引节点：<strong>i（index）+node</strong>。 每个文件都有一个 inode，存储文件的元信息。</p>
<p>简单总结一下：</p>
<ul>
<li>&#x3D;&#x3D;<strong>inode</strong> ：记录文件的属性信息，可以使用 stat 命令查看 inode 信息。&#x3D;&#x3D;</li>
<li><strong>block</strong> ：实际文件的内容，如果一个文件大于一个块时候，那么将占用多个 block，但是一个块只能存放一个文件。（因为数据是由 inode 指向的，如果有两个文件的数据存放在同一个块中，就会乱套了）</li>
</ul>
<hr>
<h5 id="Linux-文件类型"><a href="#Linux-文件类型" class="headerlink" title="Linux 文件类型"></a>Linux 文件类型</h5><p>Linux 支持很多文件类型，其中非常重要的文件类型有: <strong>普通文件</strong>，<strong>目录文件</strong>，<strong>链接文件</strong>，<strong>设备文件</strong>，<strong>管道文件</strong>，<strong>Socket 套接字文件</strong>等。</p>
<p> <strong>普通文件（-）</strong> ： 用于存储信息和数据， Linux 用户可以根据访问权限对普通文件进行查看、更改和删除。比如：图片、声音、PDF、text、视频、源代码等等。</p>
<p><strong>目录文件（d，directory file）</strong> ：目录也是文件的一种，用于表示和管理系统中的文件，目录文件中包含一些文件名和子目录名。打开目录事实上就是打开目录文件。</p>
<p><strong>符号链接文件（l，symbolic link）</strong> ：保留了指向文件的地址而不是文件本身。</p>
<p><strong>字符设备（c，char）</strong> ：用来访问字符设备比如键盘。</p>
<p><strong>设备文件（b，block）</strong> ： 用来访问块设备比如硬盘、软盘。</p>
<p><strong>管道文件(p,pipe)</strong> : 一种特殊类型的文件，用于进程之间的通信。</p>
<p><strong>套接字(s,socket)</strong> ：用于进程间的网络通信，也可以用于本机之间的非网络通信</p>
<hr>
<h5 id="Linux-目录树"><a href="#Linux-目录树" class="headerlink" title="Linux 目录树"></a>Linux 目录树</h5><p><strong>常见目录说明：</strong></p>
<ul>
<li><strong>&#x2F;bin：</strong> 存放二进制可执行文件(ls、cat、mkdir 等)，常用命令一般都在这里；</li>
<li><strong>&#x2F;etc：</strong> 存放系统管理和配置文件；</li>
<li><strong>&#x2F;home：</strong> 存放所有用户文件的根目录，是用户主目录的基点，比如用户 user 的主目录就是&#x2F;home&#x2F;user，可以用~user 表示；</li>
<li><strong>&#x2F;usr ：</strong> 用于存放系统应用程序；</li>
<li><strong>&#x2F;opt：</strong> 额外安装的可选应用程序包所放置的位置。一般情况下，我们可以把 tomcat 等都安装到这里；</li>
<li><strong>&#x2F;proc：</strong> 虚拟文件系统目录，是系统内存的映射。可直接访问这个目录来获取系统信息；</li>
<li><strong>&#x2F;root：</strong> 超级用户（系统管理员）的主目录（特权阶级^o^）；</li>
<li><strong>&#x2F;sbin:</strong> 存放二进制可执行文件，只有 root 才能访问。这里存放的是系统管理员使用的系统级别的管理命令和程序。如 ifconfig 等；</li>
<li><strong>&#x2F;dev：</strong> 用于存放设备文件；</li>
<li><strong>&#x2F;mnt：</strong> 系统管理员安装临时文件系统的安装点，系统提供这个目录是让用户临时挂载其他的文件系统；</li>
<li><strong>&#x2F;boot：</strong> 存放用于系统引导时使用的各种文件；</li>
<li><strong>&#x2F;lib ：</strong> 存放着和系统运行相关的库文件 ；</li>
<li><strong>&#x2F;tmp：</strong> 用于存放各种临时文件，是公用的临时文件存储点；</li>
<li><strong>&#x2F;var：</strong> 用于存放运行时需要改变数据的文件，也是某些大文件的溢出区，比方说各种服务的日志文件（系统启动日志等。）等；</li>
<li><strong>&#x2F;lost+found：</strong> 这个目录平时是空的，系统非正常关机而留下“无家可归”的文件（windows 下叫什么.chk）就在这里。</li>
</ul>
<hr>
<h4 id="Linux-基本命令"><a href="#Linux-基本命令" class="headerlink" title="Linux 基本命令"></a>Linux 基本命令</h4><p>Linux 命令在线速查手册：<a target="_blank" rel="noopener" href="https://www.w3xue.com/manual/linux/">https://www.w3xue.com/manual/linux/</a> 。</p>
<p>另外，<a target="_blank" rel="noopener" href="https://www.shell.how/">shell.howopen in new window</a> 这个网站可以用来解释常见命令的意思，对你学习 Linux 基本命令以及其他常用命令（如 Git、NPM）。</p>
<hr>
<h5 id="目录切换命令"><a href="#目录切换命令" class="headerlink" title="目录切换命令"></a>目录切换命令</h5><ul>
<li><strong><code>cd usr</code>：</strong> 切换到该目录下 usr 目录</li>
<li><strong><code>cd ..（或cd../）</code>：</strong> 切换到上一层目录</li>
<li><strong><code>cd /</code>：</strong> 切换到系统根目录</li>
<li><strong><code>cd ~</code>：</strong> 切换到用户主目录</li>
<li><strong><code>cd -</code>：</strong> 切换到上一个操作所在目录</li>
</ul>
<h5 id="目录的操作命令-增删改查"><a href="#目录的操作命令-增删改查" class="headerlink" title="目录的操作命令(增删改查)"></a>目录的操作命令(增删改查)</h5><p><strong><code>mkdir 目录名称</code>：</strong> 增加目录。</p>
<p>**<code>ls/ll</code>**（ll 是 ls -l 的别名，ll 命令可以看到该目录下的所有目录和文件的详细信息）：查看目录信息。</p>
<p><strong><code>find 目录 参数</code>：</strong> 寻找目录（查）。示例：① 列出当前目录及子目录下所有文件和文件夹: <code>find .</code>；② 在<code>/home</code>目录下查找以.txt 结尾的文件名:<code>find /home -name &quot;*.txt&quot;</code> ,忽略大小写: <code>find /home -iname &quot;*.txt&quot;</code> ；③ 当前目录及子目录下查找所有以.txt 和.pdf 结尾的文件:<code>find . \( -name &quot;*.txt&quot; -o -name &quot;*.pdf&quot; \)</code>或<code>find . -name &quot;*.txt&quot; -o -name &quot;*.pdf&quot;</code>。</p>
<p><strong><code>mv 目录名称 新目录名称</code>：</strong> 修改目录的名称（改）。注意：mv 的语法不仅可以对目录进行重命名而且也可以对各种文件，压缩包等进行 重命名的操作。mv 命令用来对文件或目录重新命名，或者将文件从一个目录移到另一个目录中。后面会介绍到 mv 命令的另一个用法。</p>
<p><strong><code>mv 目录名称 目录的新位置</code>：</strong> 移动目录的位置—剪切（改）。注意：mv 语法不仅可以对目录进行剪切操作，对文件和压缩包等都可执行剪切操作。另外 mv 与 cp 的结果不同，mv 好像文件“搬家”，文件个数并未增加。而 cp 对文件进行复制，文件个数增加了。</p>
<p><strong><code>cp -r 目录名称 目录拷贝的目标位置</code>：</strong> 拷贝目录（改），-r 代表递归拷贝 。注意：cp 命令不仅可以拷贝目录还可以拷贝文件，压缩包等，拷贝文件和压缩包时不 用写-r 递归。</p>
<p><strong><code>rm [-rf] 目录</code> :</strong> 删除目录（删）。注意：rm 不仅可以删除目录，也可以删除其他文件或压缩包，为了增强大家的记忆， 无论删除任何目录或文件，都直接使用<code>rm -rf</code> 目录&#x2F;文件&#x2F;压缩包。</p>
<hr>
<h5 id="文件的操作命令-增删改查"><a href="#文件的操作命令-增删改查" class="headerlink" title="文件的操作命令(增删改查)"></a>文件的操作命令(增删改查)</h5><ul>
<li><strong><code>touch 文件名称</code>:</strong> 文件的创建（增）。</li>
<li><strong><code>cat/more/less/tail 文件名称</code></strong> ：文件的查看（查） 。命令 <code>tail -f 文件</code> 可以对某个文件进行动态监控，例如 tomcat 的日志文件， 会随着程序的运行，日志会变化，可以使用 <code>tail -f catalina-2016-11-11.log</code> 监控 文 件的变化 。</li>
<li><strong><code>vim 文件</code>：</strong> 修改文件的内容（改）。vim 编辑器是 Linux 中的强大组件，是 vi 编辑器的加强版，vim 编辑器的命令和快捷方式有很多，但此处不一一阐述，大家也无需研究的很透彻，使用 vim 编辑修改文件的方式基本会使用就可以了。在实际开发中，使用 vim 编辑器主要作用就是修改配置文件，下面是一般步骤： <code>vim 文件------&gt;进入文件-----&gt;命令模式------&gt;按i进入编辑模式-----&gt;编辑文件 -------&gt;按Esc进入底行模式-----&gt;输入：wq/q!</code> （&#x3D;&#x3D;输入 wq 代表写入内容并退出，即保存；输入 q!代表强制退出不保存&#x3D;&#x3D;）。</li>
<li><strong><code>rm -rf 文件</code>：</strong> 删除文件（删）。</li>
</ul>
<hr>
<h5 id="压缩文件的操作命令"><a href="#压缩文件的操作命令" class="headerlink" title="压缩文件的操作命令"></a>压缩文件的操作命令</h5><p><strong>1）打包并压缩文件：</strong></p>
<p>Linux 中的打包文件一般是以.tar 结尾的，压缩的命令一般是以.gz 结尾的。而一般情况下打包和压缩是一起进行的，打包并压缩后的文件的后缀名一般.tar.gz。 命令：<code>tar -zcvf 打包压缩后的文件名 要打包压缩的文件</code> ，其中：</p>
<ul>
<li>z：调用 gzip 压缩命令进行压缩</li>
<li>c：打包文件</li>
<li>v：显示运行过程</li>
<li>f：指定文件名</li>
</ul>
<hr>
<p><strong>2）解压压缩包：</strong></p>
<p>命令：<code>tar [-xvf] 压缩文件</code></p>
<p>其中：x：代表解压</p>
<h5 id="x3D-x3D-Linux-的权限命令-x3D-x3D"><a href="#x3D-x3D-Linux-的权限命令-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;Linux 的权限命令&#x3D;&#x3D;"></a>&#x3D;&#x3D;Linux 的权限命令&#x3D;&#x3D;</h5><p>通过 <strong><code>ls -l</code></strong> 命令我们可以 查看某个目录下的文件或目录的权限</p>
<p><img src="https://javaguide.cn/assets/Linux%E6%9D%83%E9%99%90%E8%A7%A3%E8%AF%BB.7c1098a0.png" alt="img"></p>
<p><strong>文件的类型：</strong></p>
<ul>
<li>d： 代表目录</li>
<li>-： 代表文件</li>
<li>l： 代表软链接（可以认为是 window 中的快捷方式）</li>
</ul>
<p><strong>Linux 中权限分为以下几种：</strong></p>
<ul>
<li>&#x3D;&#x3D;r：代表权限是可读，r 也可以用数字 4 表示&#x3D;&#x3D;</li>
<li>&#x3D;&#x3D;w：代表权限是可写，w 也可以用数字 2 表示&#x3D;&#x3D;</li>
<li>&#x3D;&#x3D;x：代表权限是可执行，x 也可以用数字 1 表示&#x3D;&#x3D;</li>
</ul>
<p>需要注意的是： <strong>超级用户可以无视普通用户的权限，即使文件目录权限是 000，依旧可以访问。</strong></p>
<p><strong>在 linux 中的每个用户必须属于一个组，不能独立于组外。在 linux 中每个文件有所有者、所在组、其它组的概念。</strong></p>
<ul>
<li><strong>所有者(u)</strong> ：一般为文件的创建者，谁创建了该文件，就天然的成为该文件的所有者，用 <code>ls ‐ahl</code> 命令可以看到文件的所有者 也可以使用 chown 用户名 文件名来修改文件的所有者 。</li>
<li><strong>文件所在组(g)</strong> ：当某个用户创建了一个文件后，这个文件的所在组就是该用户所在的组用 <code>ls ‐ahl</code>命令可以看到文件的所有组也可以使用 chgrp 组名 文件名来修改文件所在的组。</li>
<li><strong>其它组(o)</strong> ：除开文件的所有者和所在组的用户外，系统的其它用户都是文件的其它组。</li>
</ul>
<hr>
<p><strong>修改文件&#x2F;目录的权限的命令：<code>chmod</code></strong></p>
<p><strong>补充一个比较常用的东西:</strong></p>
<p>假如我们装了一个 zookeeper，我们每次开机到要求其自动启动该怎么办？</p>
<ol>
<li>新建一个脚本 zookeeper</li>
<li>为新建的脚本 zookeeper 添加可执行权限，命令是:<code>chmod +x zookeeper</code></li>
<li>把 zookeeper 这个脚本添加到开机启动项里面，命令是：<code>chkconfig --add zookeeper</code></li>
<li>如果想看看是否添加成功，命令是：<code>chkconfig --list</code></li>
</ol>
<hr>
<h5 id="Linux-用户管理"><a href="#Linux-用户管理" class="headerlink" title="Linux 用户管理"></a>Linux 用户管理</h5><p><strong>Linux 用户管理相关命令:</strong></p>
<ul>
<li><code>useradd 选项 用户名</code>:添加用户账号</li>
<li><code>userdel 选项 用户名</code>:删除用户帐号</li>
<li><code>usermod 选项 用户名</code>:修改帐号</li>
<li><code>passwd 用户名</code>:更改或创建用户的密码</li>
<li><code>passwd -S 用户名</code> :显示用户账号密码信息</li>
<li><code>passwd -d 用户名</code>: 清除用户密码</li>
</ul>
<hr>
<h5 id="Linux-系统用户组的管理"><a href="#Linux-系统用户组的管理" class="headerlink" title="Linux 系统用户组的管理"></a>Linux 系统用户组的管理</h5><p>用户组的管理涉及用户组的添加、删除和修改。组的增加、删除和修改实际上就是对<code>/etc/group</code>文件的更新。</p>
<p><strong>Linux 系统用户组的管理相关命令:</strong></p>
<ul>
<li><code>groupadd 选项 用户组</code> :增加一个新的用户组</li>
<li><code>groupdel 用户组</code>:要删除一个已有的用户组</li>
<li><code>groupmod 选项 用户组</code> : 修改用户组的属性</li>
</ul>
<hr>
<h5 id="其他常用命令"><a href="#其他常用命令" class="headerlink" title="其他常用命令"></a>其他常用命令</h5><p><strong><code>pwd</code>：</strong> 显示当前所在位置</p>
<p><code>sudo + 其他命令</code>：以系统管理者的身份执行指令，也就是说，经由 sudo 所执行的指令就好像是 root 亲自执行。</p>
<p><strong><code>grep 要搜索的字符串 要搜索的文件 --color</code>：</strong> 搜索命令，–color 代表高亮显示</p>
<p><strong><code>ps -ef</code>&#x2F;<code>ps -aux</code>：</strong> 这两个命令都是查看当前系统正在运行进程，两者的区别是展示格式不同。如果想要查看特定的进程可以使用这样的格式：**<code>ps aux|grep redis</code>** （查看包括 redis 字符串的进程），也可使用 <code>pgrep redis -a</code>。</p>
<p>注意：如果直接用 ps（（Process Status））命令，会显示所有进程的状态，通常结合 grep 命令查看某进程的状态。</p>
<p><strong><code>kill -9 进程的pid</code>：</strong> 杀死进程（-9 表示强制终止。）</p>
<p>先用 ps 查找进程，然后用 kill 杀掉</p>
<p><strong>网络通信命令：</strong></p>
<ul>
<li>查看当前系统的网卡信息：ifconfig</li>
<li>查看与某台机器的连接情况：ping</li>
<li>查看当前系统的端口使用：netstat -an</li>
</ul>
<p> <strong>net-tools 和 iproute2 ：****net-tools 和 iproute2 ：</strong><code>net-tools</code>起源于 BSD 的 TCP&#x2F;IP 工具箱，后来成为老版本 LinuxLinux 中配置网络功能的工具。但自 2001 年起，Linux 社区已经对其停止维护。同时，一些 Linux 发行版比如 Arch Linux 和 CentOS&#x2F;RHEL 7 则已经完全抛弃了 net-tools，只支持<code>iproute2</code>。linux ip 命令类似于 ifconfig，但功能更强大，旨在替代它。</p>
<p><strong><code>shutdown</code>：</strong><code>shutdown -h now</code>： 指定现在立即关机；<code>shutdown +5 &quot;System will shutdown after 5 minutes&quot;</code>：指定 5 分钟后关机，同时送出警告信息给登入用户。</p>
<p><strong><code>reboot</code>：</strong> <strong><code>reboot</code>：</strong> 重开机。**<code>reboot -w</code>：** 做个重开机的模拟（只有纪录并不会真的重开机）。</p>
<hr>
<h4 id="Linux-环境变量"><a href="#Linux-环境变量" class="headerlink" title="Linux 环境变量"></a>Linux 环境变量</h4><h5 id="环境变量分类"><a href="#环境变量分类" class="headerlink" title="环境变量分类"></a>环境变量分类</h5><p>按照作用域来分，环境变量可以简单的分成:</p>
<ul>
<li>用户级别环境变量 : <code>~/.bashrc</code>、<code>~/.bash_profile</code>。</li>
<li>系统级别环境变量 : <code>/etc/bashrc</code>、<code>/etc/environment</code>、<code>/etc/profile</code>、<code>/etc/profile.d</code>。</li>
</ul>
<p>按照生命周期来分，环境变量可以简单的分成:</p>
<ul>
<li>永久的：需要用户修改相关的配置文件，变量永久生效。</li>
<li>临时的：用户利用 <code>export</code> 命令，在当前终端下声明环境变量，关闭 shell 终端失效。</li>
</ul>
<hr>
<h5 id="读取环境变量"><a href="#读取环境变量" class="headerlink" title="读取环境变量"></a>读取环境变量</h5><p>通过 <code>export</code> 命令可以输出当前系统定义的所有环境变量。</p>
<p><code>env</code> 命令也可以列出所有环境变量。</p>
<p><code>echo</code> 命令可以输出指定环境变量的值。</p>
<hr>
<h5 id="环境变量修改"><a href="#环境变量修改" class="headerlink" title="环境变量修改"></a>环境变量修改</h5><p>通过 <code>export</code>命令可以修改指定的环境变量。不过，这种方式修改环境变量仅仅对当前 shell 终端生效，关闭 shell 终端就会失效。修改完成之后，立即生效。</p>
<p>通过 <code>vim</code> 命令修改环境变量配置文件。这种方式修改环境变量永久有效。</p>
<p>如果修改的是系统级别环境变量则对所有用户生效，如果修改的是用户级别环境变量则仅对当前用户生效。</p>
<p>修改完成之后，需要 <code>source</code> 命令让其生效或者关闭 shell 终端重新登录。</p>
<hr>
<h4 id="Shell-编程基础知识总结"><a href="#Shell-编程基础知识总结" class="headerlink" title="Shell 编程基础知识总结"></a>Shell 编程基础知识总结</h4><h5 id="走进-Shell-编程的大门"><a href="#走进-Shell-编程的大门" class="headerlink" title="走进 Shell 编程的大门"></a>走进 Shell 编程的大门</h5><h6 id="为什么要学-Shell？"><a href="#为什么要学-Shell？" class="headerlink" title="为什么要学 Shell？"></a>为什么要学 Shell？</h6><p>目前 Linux 系统下最流行的运维自动化语言就是 Shell 和 Python 了。</p>
<p>Shell 是一个命令解释器，解释执行用户所输入的命令和程序。一输入命令，就立即回应的交互的对话方式。</p>
<h6 id="什么是-Shell？"><a href="#什么是-Shell？" class="headerlink" title="什么是 Shell？"></a>什么是 Shell？</h6><p>首先让我们从下图看看 Shell 在整个操作系统中所处的位置吧，该图的外圆描述了整个操作系统(比如Debian&#x2F;Ubuntu&#x2F;Slackware 等)，内圆描述了操作系统的核心(比如 Linux Kernel )，而 Shell 和GUI 一样作为用户和操作系统之间的接口.</p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230303102209654.png" alt="image-20230303102209654"></p>
<p>GUI 提供了一种图形化的用户接口，使用起来非常简便易学;而 shel1 则为用户提供了一种命令行的接口，接收用户的键盘输入，并分析和执行输入字符串中的命令，然后给用户返回执行结果，使用起来可能会复杂些，但是由于占用的资源少，而且在操作熟练以后可能会提高工作效率，而且具有批处理的功能，因此在某些应用场合还非常流行</p>
<h6 id="Shell-编程的-Hello-World"><a href="#Shell-编程的-Hello-World" class="headerlink" title="Shell 编程的 Hello World"></a>Shell 编程的 Hello World</h6><p>shell 中 # 符号表示注释。<strong>shell 的第一行比较特殊，一般都会以#!开始来指定使用的 shell 类型。在 linux 中，除了 bash shell 以外，还有很多版本的 shell， 例如 zsh、dash 等等…不过 bash shell 还是我们使用最多的。</strong></p>
<hr>
<h5 id="Shell-变量"><a href="#Shell-变量" class="headerlink" title="Shell 变量"></a>Shell 变量</h5><h6 id="Shell-编程中的变量介绍"><a href="#Shell-编程中的变量介绍" class="headerlink" title="Shell 编程中的变量介绍"></a>Shell 编程中的变量介绍</h6><p><strong>Shell 编程中一般分为三种变量：</strong></p>
<ol>
<li><strong>我们自己定义的变量（自定义变量）:</strong> 仅在当前 Shell 实例中有效，其他 Shell 启动的程序不能访问局部变量。</li>
<li><strong>Linux 已定义的环境变量</strong>（环境变量， 例如：<code>PATH</code>, <code>HOME</code> 等…, 这类变量我们可以直接使用），使用 <code>env</code> 命令可以查看所有的环境变量，而 set 命令既可以查看环境变量也可以查看自定义变量。</li>
<li><strong>Shell 变量</strong> ：Shell 变量是由 Shell 程序设置的特殊变量。Shell 变量中有一部分是环境变量，有一部分是局部变量，这些变量保证了 Shell 的正常运行</li>
</ol>
<p><strong>常用的环境变量:</strong></p>
<blockquote>
<p>PATH 决定了 shell 将到哪些目录中寻找命令或程序<br>HOME 当前用户主目录<br>HISTSIZE 　历史记录数<br>LOGNAME 当前用户的登录名<br>HOSTNAME 　指主机的名称<br>SHELL 当前用户 Shell 类型<br>LANGUAGE 　语言相关的环境变量，多语言可以修改此环境变量<br>MAIL 　当前用户的邮件存放目录<br>PS1 　基本提示符，对于 root 用户是#，对于普通用户是$</p>
</blockquote>
<p><strong>使用 Linux 已定义的环境变量：</strong></p>
<p>比如我们要看当前用户目录可以使用：<code>echo $HOME</code>命令；如果我们要看当前用户 Shell 类型 可以使用<code>echo $SHELL</code>命令。可以看出，使用方法非常简单。</p>
<p><strong>使用自己定义的变量：</strong></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">#!/bin/bash</span><br><span class="line">#自定义变量hello</span><br><span class="line">hello=&quot;hello world&quot;</span><br><span class="line">echo $hello</span><br><span class="line">echo  &quot;helloworld!&quot;</span><br></pre></td></tr></table></figure>

<p><strong>Shell 编程中的变量名的命名的注意事项：</strong></p>
<ul>
<li>命名只能使用英文字母，数字和下划线，首个字符不能以数字开头，但是可以使用下划线（_）开头。</li>
<li>中间不能有空格，可以使用下划线（_）。</li>
<li>不能使用标点符号。</li>
<li>不能使用 bash 里的关键字（可用 help 命令查看保留关键字）。</li>
</ul>
<hr>
<h6 id="Shell-字符串入门"><a href="#Shell-字符串入门" class="headerlink" title="Shell 字符串入门"></a>Shell 字符串入门</h6><p>字符串是 shell 编程中最常用最有用的数据类型（除了数字和字符串，也没啥其它类型好用了），字符串可以用单引号，也可以用双引号。</p>
<p>在单引号中所有的特殊符号，如$和反引号都没有特殊含义。在双引号中，除了”$”,””和反引号，其他的字符没有特殊含义。</p>
<hr>
<h6 id="Shell-字符串常见操作"><a href="#Shell-字符串常见操作" class="headerlink" title="Shell 字符串常见操作"></a>Shell 字符串常见操作</h6><p>使用 expr 命令时，表达式中的运算符左右必须包含空格，如果不包含空格，将会输出表达式本身:</p>
<p>对于某些运算符，还需要我们使用符号<code>\</code>进行转义，否则就会提示语法错误。</p>
<hr>
<h6 id="Shell-数组"><a href="#Shell-数组" class="headerlink" title="Shell 数组"></a>Shell 数组</h6><p>bash 支持一维数组（不支持多维数组），并且没有限定数组的大小。我下面给了大家一个关于数组操作的 Shell 代码示例，通过该示例大家可以知道如何创建数组、获取数组长度、获取&#x2F;删除特定位置的数组元素、删除整个数组以及遍历数组。</p>
<hr>
<h5 id="Shell-基本运算符"><a href="#Shell-基本运算符" class="headerlink" title="Shell 基本运算符"></a>Shell 基本运算符</h5><p>Shell 编程支持下面几种运算符</p>
<ul>
<li>算数运算符</li>
<li>关系运算符</li>
<li>布尔运算符</li>
<li>字符串运算符</li>
<li>文件测试运算符</li>
</ul>
<h6 id="关系运算符"><a href="#关系运算符" class="headerlink" title="关系运算符"></a>关系运算符</h6><p><img src="E:\XxdBlog\source_posts\images\test\image-20230303131632822.png" alt="image-20230303131632822"></p>
<h6 id="布尔运算符"><a href="#布尔运算符" class="headerlink" title="布尔运算符"></a>布尔运算符</h6><p><img src="E:\XxdBlog\source_posts\images\test\image-20230303131739253.png" alt="image-20230303131739253"></p>
<h6 id="字符串运算符"><a href="#字符串运算符" class="headerlink" title="字符串运算符"></a>字符串运算符</h6><p><img src="E:\XxdBlog\source_posts\images\test\image-20230303131825928.png" alt="image-20230303131825928"></p>
<h6 id="文件相关运算符"><a href="#文件相关运算符" class="headerlink" title="文件相关运算符"></a>文件相关运算符</h6><p><img src="E:\XxdBlog\source_posts\images\test\image-20230303132248344.png" alt="image-20230303132248344"></p>
<h5 id="Shell-流程控制"><a href="#Shell-流程控制" class="headerlink" title="Shell 流程控制"></a>Shell 流程控制</h5><h6 id="if-条件语句"><a href="#if-条件语句" class="headerlink" title="if 条件语句"></a>if 条件语句</h6><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">if [ $a -eq $b ]</span><br><span class="line">then</span><br><span class="line">   echo &quot;a 等于 b&quot;</span><br><span class="line">elif [ $a -gt $b ]</span><br><span class="line">then</span><br><span class="line">   echo &quot;a 大于 b&quot;</span><br><span class="line">else</span><br><span class="line">   echo &quot;a 小于 b&quot;</span><br><span class="line">fi</span><br></pre></td></tr></table></figure>

<h6 id="for-循环语句"><a href="#for-循环语句" class="headerlink" title="for 循环语句"></a>for 循环语句</h6><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">for loop in 1 2 3 4 5</span><br><span class="line">do</span><br><span class="line">    echo &quot;The value is: $loop&quot;</span><br><span class="line">done</span><br></pre></td></tr></table></figure>

<h6 id="while-语句"><a href="#while-语句" class="headerlink" title="while 语句"></a>while 语句</h6><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">#!/bin/bash</span><br><span class="line">int=1</span><br><span class="line">while(( $int&lt;=5 ))</span><br><span class="line">do</span><br><span class="line">    echo $int</span><br><span class="line">    let &quot;int++&quot;</span><br><span class="line">done</span><br></pre></td></tr></table></figure>

<p><strong>while 循环可用于读取键盘信息：</strong></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">echo &#x27;按下 &lt;CTRL-D&gt; 退出&#x27;</span><br><span class="line">echo -n &#x27;输入你最喜欢的电影: &#x27;</span><br><span class="line">while read FILM</span><br><span class="line">do</span><br><span class="line">    echo &quot;是的！$FILM 是一个好电影&quot;</span><br><span class="line">done</span><br></pre></td></tr></table></figure>

<h5 id="Shell-函数"><a href="#Shell-函数" class="headerlink" title="Shell 函数"></a>Shell 函数</h5><h6 id="不带参数没有返回值的函数"><a href="#不带参数没有返回值的函数" class="headerlink" title="不带参数没有返回值的函数"></a>不带参数没有返回值的函数</h6><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">#!/bin/bash</span><br><span class="line">hello()&#123;</span><br><span class="line">    echo &quot;这是我的第一个 shell 函数!&quot;</span><br><span class="line">&#125;</span><br><span class="line">echo &quot;-----函数开始执行-----&quot;</span><br><span class="line">hello</span><br><span class="line">echo &quot;-----函数执行完毕-----&quot;</span><br></pre></td></tr></table></figure>

<h6 id="有返回值的函数"><a href="#有返回值的函数" class="headerlink" title="有返回值的函数"></a>有返回值的函数</h6><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">#!/bin/bash</span><br><span class="line">funWithReturn()&#123;</span><br><span class="line">    echo &quot;输入第一个数字: &quot;</span><br><span class="line">    read aNum</span><br><span class="line">    echo &quot;输入第二个数字: &quot;</span><br><span class="line">    read anotherNum</span><br><span class="line">    echo &quot;两个数字分别为 $aNum 和 $anotherNum !&quot;</span><br><span class="line">    return $(($aNum+$anotherNum))</span><br><span class="line">&#125;</span><br><span class="line">funWithReturn</span><br><span class="line">echo &quot;输入的两个数字之和为 $?&quot;</span><br></pre></td></tr></table></figure>

<h6 id="带参数的函数"><a href="#带参数的函数" class="headerlink" title="带参数的函数"></a>带参数的函数</h6><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">#!/bin/bash</span><br><span class="line">funWithParam()&#123;</span><br><span class="line">    echo &quot;第一个参数为 $1 !&quot;</span><br><span class="line">    echo &quot;第二个参数为 $2 !&quot;</span><br><span class="line">    echo &quot;第十个参数为 $10 !&quot;</span><br><span class="line">    echo &quot;第十个参数为 $&#123;10&#125; !&quot;</span><br><span class="line">    echo &quot;第十一个参数为 $&#123;11&#125; !&quot;</span><br><span class="line">    echo &quot;参数总数有 $# 个!&quot;</span><br><span class="line">    echo &quot;作为一个字符串输出所有参数 $* !&quot;</span><br><span class="line">&#125;</span><br><span class="line">funWithParam 1 2 3 4 5 6 7 8 9 34 73</span><br></pre></td></tr></table></figure>

<p>输出结果：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">第一个参数为 1 !</span><br><span class="line">第二个参数为 2 !</span><br><span class="line">第十个参数为 10 !</span><br><span class="line">第十个参数为 34 !</span><br><span class="line">第十一个参数为 73 !</span><br><span class="line">参数总数有 11 个!</span><br><span class="line">作为一个字符串输出所有参数 1 2 3 4 5 6 7 8 9 34 73 !</span><br></pre></td></tr></table></figure>

<hr>
<h2 id="数据结构"><a href="#数据结构" class="headerlink" title="数据结构"></a>数据结构</h2><h3 id="线性数据结构-数组、链表、栈、队列"><a href="#线性数据结构-数组、链表、栈、队列" class="headerlink" title="线性数据结构 :数组、链表、栈、队列"></a>线性数据结构 :数组、链表、栈、队列</h3><h4 id="数组"><a href="#数组" class="headerlink" title="数组"></a>数组</h4><p><strong>数组（Array）</strong> 是一种很常见的数据结构。它由相同类型的元素（element）组成，并且是使用一块连续的内存来存储。</p>
<p>数组的特点是：<strong>提供随机访问</strong> 并且容量有限。</p>
<h4 id="链表"><a href="#链表" class="headerlink" title="链表"></a>链表</h4><h5 id="2-1-链表简介"><a href="#2-1-链表简介" class="headerlink" title="2.1. 链表简介"></a>2.1. 链表简介</h5><p><strong>链表（LinkedList）</strong> 虽然是一种线性表，但是并不会按线性的顺序存储数据，使用的不是连续的内存空间来存储数据。</p>
<h5 id="2-2-链表分类"><a href="#2-2-链表分类" class="headerlink" title="2.2. 链表分类"></a>2.2. 链表分类</h5><ol>
<li>单链表</li>
<li>双向链表</li>
<li>循环链表</li>
<li>双向循环链表</li>
</ol>
<h6 id="2-2-1-单链表"><a href="#2-2-1-单链表" class="headerlink" title="2.2.1. 单链表"></a>2.2.1. 单链表</h6><h6 id="2-2-2-循环链表"><a href="#2-2-2-循环链表" class="headerlink" title="2.2.2. 循环链表"></a>2.2.2. 循环链表</h6><p><strong>循环链表</strong> 其实是一种特殊的单链表，和单链表不同的是循环链表的尾结点不是指向 null，而是指向链表的头结点。</p>
<h5 id="2-4-数组-vs-链表"><a href="#2-4-数组-vs-链表" class="headerlink" title="2.4. 数组 vs 链表"></a>2.4. 数组 vs 链表</h5><ul>
<li>数组支持随机访问，而链表不支持。</li>
<li>数组使用的是连续内存空间对 CPU 的缓存机制友好，链表则相反。</li>
<li>数组的大小固定，而链表则天然支持动态扩容。如果声明的数组过小，需要另外申请一个更大的内存空间存放数组元素，然后将原数组拷贝进去，这个操作是比较耗时的！</li>
</ul>
<hr>
<h4 id="栈"><a href="#栈" class="headerlink" title="栈"></a>栈</h4><h5 id="3-1-栈简介"><a href="#3-1-栈简介" class="headerlink" title="3.1. 栈简介"></a>3.1. 栈简介</h5><p><strong>栈</strong> (stack)只允许在有序的线性数据集合的一端（称为栈顶 top）进行加入数据（push）和移除数据（pop）。因而按照 <strong>后进先出（LIFO, Last In First Out）</strong> 的原理运作。<strong>在栈中，push 和 pop 的操作都发生在栈顶。</strong></p>
<p>栈常用一维数组或链表来实现，用数组实现的栈叫作 <strong>顺序栈</strong> ，用链表实现的栈叫作 <strong>链式栈</strong> 。</p>
<hr>
<h5 id="3-2-栈的常见应用常见应用场景"><a href="#3-2-栈的常见应用常见应用场景" class="headerlink" title="3.2. 栈的常见应用常见应用场景"></a>3.2. 栈的常见应用常见应用场景</h5><h6 id="3-2-1-实现浏览器的回退和前进功能"><a href="#3-2-1-实现浏览器的回退和前进功能" class="headerlink" title="3.2.1. 实现浏览器的回退和前进功能"></a>3.2.1. 实现浏览器的回退和前进功能</h6><h6 id="3-2-2-检查符号是否成对出现"><a href="#3-2-2-检查符号是否成对出现" class="headerlink" title="3.2.2. 检查符号是否成对出现"></a>3.2.2. 检查符号是否成对出现</h6><h6 id="3-2-3-反转字符串"><a href="#3-2-3-反转字符串" class="headerlink" title="3.2.3. 反转字符串"></a>3.2.3. 反转字符串</h6><h6 id="3-2-4-维护函数调用"><a href="#3-2-4-维护函数调用" class="headerlink" title="3.2.4. 维护函数调用"></a>3.2.4. 维护函数调用</h6><hr>
<h5 id="3-3-栈的实现"><a href="#3-3-栈的实现" class="headerlink" title="3.3. 栈的实现"></a>3.3. 栈的实现</h5><hr>
<h4 id="队列"><a href="#队列" class="headerlink" title="队列"></a>队列</h4><h5 id="4-1-队列简介"><a href="#4-1-队列简介" class="headerlink" title="4.1. 队列简介"></a>4.1. 队列简介</h5><p><strong>队列只允许在后端（rear）进行插入操作也就是 入队 enqueue，在前端（front）进行删除操作也就是出队 dequeue</strong></p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230303143512374.png" alt="image-20230303143512374"></p>
<h5 id="4-2-队列分类"><a href="#4-2-队列分类" class="headerlink" title="4.2. 队列分类"></a>4.2. 队列分类</h5><h6 id="4-2-1-单队列"><a href="#4-2-1-单队列" class="headerlink" title="4.2.1. 单队列"></a>4.2.1. 单队列</h6><p>单队列又分为 <strong>顺序队列（数组实现）</strong> 和 <strong>链式队列（链表实现）</strong>。</p>
<p><strong>顺序队列存在“假溢出”的问题也就是明明有位置却不能添加的情况。</strong></p>
<p>入队与出队头指针与尾指针都向后移动。</p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230303144134674.png" alt="image-20230303144134674"></p>
<p>尾指针移动到我们可以进行队列操作的范围之外去了，我们称呼作为队列用的存储区还没有满,但队列却发生了溢出,我们把这种现象称为”假溢出”。 </p>
<h6 id="4-2-2-循环队列"><a href="#4-2-2-循环队列" class="headerlink" title="4.2.2. 循环队列"></a>4.2.2. 循环队列</h6><p>循环队列可以解决顺序队列的假溢出和越界问题。解决办法就是：从头开始，这样也就会形成头尾相接的循环，这也就是循环队列名字的由来。</p>
<h5 id="4-3-常见应用场景"><a href="#4-3-常见应用场景" class="headerlink" title="4.3. 常见应用场景"></a>4.3. 常见应用场景</h5><ul>
<li><strong>阻塞队列：</strong> 阻塞队列可以看成在队列基础上加了阻塞操作的队列。当队列为空的时候，出队操作阻塞，当队列满的时候，入队操作阻塞。使用阻塞队列我们可以很容易实现“生产者 - 消费者“模型。</li>
<li><strong>线程池中的请求&#x2F;任务队列：</strong> 线程池中没有空闲线程时，新的任务请求线程资源时，线程池该如何处理呢？答案是将这些请求放在队列中，当有空闲线程的时候，会循环中反复从队列中获取任务来执行。队列分为无界队列(基于链表)和有界队列(基于数组)。无界队列的特点就是可以一直入列，除非系统资源耗尽，比如 ：<code>FixedThreadPool</code> 使用无界队列 <code>LinkedBlockingQueue</code>。但是有界队列就不一样了，当队列满的话后面再有任务&#x2F;请求就会拒绝，在 Java 中的体现就是会抛出<code>java.util.concurrent.RejectedExecutionException</code> 异常。</li>
<li>Linux 内核进程队列（按优先级排队）</li>
<li>现实生活中的派对，播放器上的播放列表;</li>
<li>消息队列</li>
</ul>
<hr>
<h4 id="图"><a href="#图" class="headerlink" title="图"></a>图</h4><p><strong>何为图呢？</strong> 简单来说，图就是由顶点的有穷非空集合和顶点之间的边组成的集合。通常表示为：**G(V,E)**，其中，G表示一个图，V表示顶点的集合，E表示边的集合。</p>
<h5 id="图的存储"><a href="#图的存储" class="headerlink" title="图的存储"></a>图的存储</h5><hr>
<h6 id="邻接矩阵存储"><a href="#邻接矩阵存储" class="headerlink" title="邻接矩阵存储"></a>邻接矩阵存储</h6><p>邻接矩阵将图用二维矩阵存储，是一种较为直观的表示方式。</p>
<p>值得注意的是：<strong>无向图的邻接矩阵是一个对称矩阵，因为在无向图中，顶点i和顶点j有关系，则顶点j和顶点i必有关系。</strong></p>
<p>邻接矩阵存储的方式优点是简单直接（直接使用一个二维数组即可），并且，在获取两个定点之间的关系的时候也非常高效（直接获取指定位置的数组元素的值即可）。但是，这种存储方式的缺点也比较明显，那就是比较浪费空间，</p>
<hr>
<h6 id="邻接表存储"><a href="#邻接表存储" class="headerlink" title="邻接表存储"></a>邻接表存储</h6><p>邻接链表使用一个链表来存储某个顶点的所有后继相邻顶点。对于图中每个顶点Vi，把所有邻接于Vi的顶点Vj链成一个单链表，这个单链表称为顶点Vi的 <strong>邻接表</strong>。</p>
<hr>
<h5 id="图的搜索"><a href="#图的搜索" class="headerlink" title="图的搜索"></a>图的搜索</h5><h6 id="广度优先搜索"><a href="#广度优先搜索" class="headerlink" title="广度优先搜索"></a>广度优先搜索</h6><p><strong>广度优先搜索的具体实现方式用到了之前所学过的线性数据结构——队列</strong> 。</p>
<h6 id="深度优先搜索"><a href="#深度优先搜索" class="headerlink" title="深度优先搜索"></a>深度优先搜索</h6><p>深度优先搜索就是“一条路走到黑”，从源顶点开始，一直走到没有后继节点，才回溯到上一顶点，然后继续“一条路走到黑”</p>
<p><strong>和广度优先搜索类似，深度优先搜索的具体实现用到了另一种线性数据结构——栈</strong> 。</p>
<hr>
<h4 id="堆-1"><a href="#堆-1" class="headerlink" title="堆"></a>堆</h4><h5 id="什么是堆"><a href="#什么是堆" class="headerlink" title="什么是堆"></a>什么是堆</h5><p>&#x3D;&#x3D;<strong>特别提示：</strong>&#x3D;&#x3D;</p>
<p>很多博客说堆是完全二叉树，其实并非如此，<strong>堆不一定是完全二叉树</strong>，只是为了方便存储和索引，我们通常用完全二叉树的形式来表示堆，事实上，广为人知的斐波那契堆和二项堆就不是完全二叉树,它们甚至都不是二叉树。</p>
<p>（<strong>二叉</strong>）堆是一个数组，它可以被看成是一个 <strong>近似的完全二叉树</strong>。——《算法导论》第三版</p>
<hr>
<h5 id="堆的用途"><a href="#堆的用途" class="headerlink" title="堆的用途"></a>堆的用途</h5><p>当我们&#x3D;&#x3D;只关心所有数据中的最大值或者最小值&#x3D;&#x3D;，存在多次获取最大值或者最小值，多次插入或删除数据时，就可以使用堆。</p>
<p><strong>相对于有序数组而言，堆的主要优势在于更新数据效率较高。</strong> 堆的初始化时间复杂度为 <code>O(nlog(n))</code>，堆可以做到<code>O(1)</code>时间复杂度取出最大值或者最小值，<code>O(log(n))</code>时间复杂度插入或者删除数据。</p>
<hr>
<h5 id="堆的分类"><a href="#堆的分类" class="headerlink" title="堆的分类"></a>堆的分类</h5><p>堆分为 <strong>最大堆</strong> 和 <strong>最小堆</strong>。二者的区别在于节点的排序方式。</p>
<ul>
<li><strong>最大堆</strong> ：堆中的每一个节点的值都大于等于子树中所有节点的值</li>
<li><strong>最小堆</strong> ：堆中的每一个节点的值都小于等于子树中所有节点的值</li>
</ul>
<hr>
<h5 id="堆的存储"><a href="#堆的存储" class="headerlink" title="堆的存储"></a>堆的存储</h5><p>之前介绍树的时候说过，由于完全二叉树的优秀性质，利用数组存储二叉树即节省空间，又方便索引（若根结点的序号为1，那么对于树中任意节点i，其左子节点序号为 <code>2*i</code>，右子节点序号为 <code>2*i+1</code>）。</p>
<h5 id="堆的操作"><a href="#堆的操作" class="headerlink" title="堆的操作"></a>堆的操作</h5><p>堆的更新操作主要包括两种 : <strong>插入元素</strong> 和 <strong>删除堆顶元素</strong>。</p>
<h6 id="插入元素"><a href="#插入元素" class="headerlink" title="插入元素"></a>插入元素</h6><p><strong>1.将要插入的元素放到最后</strong></p>
<p><strong>2.从底向上，如果父结点比该元素小，则该节点和父结点交换，直到无法交换</strong></p>
<h6 id="删除堆顶元素"><a href="#删除堆顶元素" class="headerlink" title="删除堆顶元素"></a>删除堆顶元素</h6><p>根据堆的性质可知，最大堆的堆顶元素为所有元素中最大的，最小堆的堆顶元素是所有元素中最小的。当我们需要多次查找最大元素或者最小元素的时候，可以利用堆来实现。</p>
<p>删除堆顶元素后，为了保持堆的性质，需要对堆的结构进行调整，我们将这个过程称之为”<strong>堆化</strong>“，堆化的方法分为两种：</p>
<ul>
<li>一种是自底向上的堆化，上述的插入元素所使用的就是自底向上的堆化，元素从最底部向上移动。</li>
<li>另一种是自顶向下堆化，元素由最顶部向下移动。在讲解删除堆顶元素的方法时，我将阐述这两种操作的过程，大家可以体会一下二者的不同。</li>
</ul>
<p><strong>自底向上堆化</strong></p>
<p>比较根结点的左子节点和右子节点，也就是下标为2,3的数组元素，将较大的元素填充到根结点(下标为1)的位置。</p>
<p><strong>自顶向下堆化</strong></p>
<p>自顶向下的堆化用一个词形容就是“石沉大海”，那么第一件事情，就是把石头抬起来，从海面扔下去。这个石头就是堆的最后一个元素，我们将最后一个元素移动到堆顶。</p>
<hr>
<h6 id="堆的操作总结"><a href="#堆的操作总结" class="headerlink" title="堆的操作总结"></a>堆的操作总结</h6><p> <strong>插入元素</strong> ：先将元素放至数组末尾，再自底向上堆化，将末尾元素上浮</p>
<p><strong>删除堆顶元素</strong> ：删除堆顶元素，将末尾元素放至堆顶，再自<strong>顶向下堆化</strong>，将堆顶元素下沉。也可以自底向上堆化，只是会产生“气泡”，浪费存储空间。最好采用自顶向下堆化的方式。</p>
<hr>
<h5 id="堆排序"><a href="#堆排序" class="headerlink" title="堆排序"></a>堆排序</h5><p>堆排序的过程分为两步：</p>
<ul>
<li>第一步是建堆，将一个无序的数组建立为一个堆</li>
<li>第二步是排序，将堆顶元素取出，然后对剩下的元素进行堆化，反复迭代，直到所有元素被取出为止。</li>
</ul>
<h6 id="建堆"><a href="#建堆" class="headerlink" title="建堆"></a>建堆</h6><p>如果你已经足够了解堆化的过程，那么建堆的过程掌握起来就比较容易了。建堆的过程就是一个对所有非叶节点的自顶向下堆化过程。</p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230303153409462.png" alt="image-20230303153409462"></p>
<p>将初始的无序数组抽象为一棵树，图中的节点个数为6，所以4,5,6节点为叶节点，1,2,3节点为非叶节点，所以要对1-3号节点进行自顶向下（沉底）堆化，注意，顺序是从后往前堆化，从3号节点开始，一直到1号节点。 3号节点堆化结果：</p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230303153436831.png" alt="image-20230303153436831"></p>
<hr>
<h6 id="排序"><a href="#排序" class="headerlink" title="排序"></a>排序</h6><p>由于堆顶元素是所有元素中最大的，所以我们重复取出堆顶元素，将这个最大的堆顶元素放至数组末尾，并对剩下的元素进行堆化（向下）即可。</p>
<hr>
<h4 id="树"><a href="#树" class="headerlink" title="树"></a>树</h4><p>一棵树具有以下特点：</p>
<ol>
<li>一棵树中的任意两个结点有且仅有唯一的一条路径连通。</li>
<li>一棵树如果有 n 个结点，那么它一定恰好有 n-1 条边。</li>
<li>一棵树不包含回路。</li>
</ol>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230303154302402.png" alt="image-20230303154302402"></p>
<ul>
<li><strong>节点</strong> ：树中的每个元素都可以统称为节点。</li>
<li><strong>根节点</strong> ：顶层节点或者说没有父节点的节点。上图中 A 节点就是根节点。</li>
<li><strong>父节点</strong> ：若一个节点含有子节点，则这个节点称为其子节点的父节点。上图中的 B 节点是 D 节点、E 节点的父节点。</li>
<li><strong>子节点</strong> ：一个节点含有的子树的根节点称为该节点的子节点。上图中 D 节点、E 节点是 B 节点的子节点。</li>
<li><strong>兄弟节点</strong> ：具有相同父节点的节点互称为兄弟节点。上图中 D 节点、E 节点的共同父节点是 B 节点，故 D 和 E 为兄弟节点。</li>
<li><strong>叶子节点</strong> ：没有子节点的节点。上图中的 D、F、H、I 都是叶子节点。</li>
<li><strong>节点的高度</strong> ：该节点到叶子节点的最长路径所包含的边数。</li>
<li><strong>节点的深度</strong> ：根节点到该节点的路径所包含的边数</li>
<li><strong>节点的层数</strong> ：节点的深度+1。</li>
<li><strong>树的高度</strong> ：根节点的高度。</li>
</ul>
<hr>
<h5 id="二叉树的分类"><a href="#二叉树的分类" class="headerlink" title="二叉树的分类"></a>二叉树的分类</h5><p><strong>二叉树</strong> 的第 i 层至多拥有 <code>2^(i-1)</code> 个节点，深度为 k 的二叉树至多总共有 <code>2^(k+1)-1</code> 个节点（满二叉树的情况），至少有 2^(k) 个节点（关于节点的深度的定义国内争议比较多，我个人比较认可维基百科对<a target="_blank" rel="noopener" href="https://zh.wikipedia.org/wiki/%E6%A0%91_(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84)#/%E6%9C%AF%E8%AF%AD">节点深度的定义open in new window</a>）。</p>
<hr>
<h6 id="满二叉树"><a href="#满二叉树" class="headerlink" title="满二叉树"></a>满二叉树</h6><p>一个二叉树，如果每一个层的结点数都达到最大值，则这个二叉树就是 <strong>满二叉树</strong>。也就是说，如果一个二叉树的层数为 K，且结点总数是(2^k) -1 ，则它就是 <strong>满二叉树</strong>。</p>
<hr>
<h6 id="完全二叉树"><a href="#完全二叉树" class="headerlink" title="完全二叉树"></a>完全二叉树</h6><p>除最后一层外，若其余层都是满的，并且最后一层或者是满的，或者是在右边缺少连续若干节点，则这个二叉树就是 <strong>完全二叉树</strong> 。</p>
<p>完全二叉树有一个很好的性质：<strong>父结点和子节点的序号有着对应关系。</strong></p>
<hr>
<h6 id="平衡二叉树"><a href="#平衡二叉树" class="headerlink" title="平衡二叉树"></a>平衡二叉树</h6><p><strong>平衡二叉树</strong> 是一棵二叉排序树，且具有以下性质：</p>
<ol>
<li>可以是一棵空树</li>
<li>如果不是空树，它的左右两个子树的高度差的绝对值不超过 1，并且左右两个子树都是一棵平衡二叉树。</li>
</ol>
<p>平衡二叉树的常用实现方法有 <strong>红黑树</strong>、<strong>AVL 树</strong>、<strong>替罪羊树</strong>、<strong>加权平衡树</strong>、<strong>伸展树</strong> 等。</p>
<hr>
<h5 id="二叉树的存储"><a href="#二叉树的存储" class="headerlink" title="二叉树的存储"></a>二叉树的存储</h5><h6 id="链式存储"><a href="#链式存储" class="headerlink" title="链式存储"></a>链式存储</h6><p>和链表类似，二叉树的链式存储依靠指针将各个节点串联起来，不需要连续的存储空间。</p>
<p>每个节点包括三个属性：</p>
<ul>
<li>数据 data。data 不一定是单一的数据，根据不同情况，可以是多个具有不同类型的数据。</li>
<li>左节点指针 left</li>
<li>右节点指针 right。</li>
</ul>
<h6 id="顺序存储"><a href="#顺序存储" class="headerlink" title="顺序存储"></a>顺序存储</h6><p>顺序存储就是利用数组进行存储，数组中的每一个位置仅存储节点的 data，不存储左右子节点的指针，子节点的索引通过数组下标完成。根结点的序号为 1，对于每个节点 Node，假设它存储在数组中下标为 i 的位置，那么它的左子节点就存储在 2i 的位置，它的右子节点存储在下标为 2i+1 的位置。</p>
<hr>
<h5 id="二叉树的遍历"><a href="#二叉树的遍历" class="headerlink" title="二叉树的遍历"></a>二叉树的遍历</h5><h6 id="先序遍历"><a href="#先序遍历" class="headerlink" title="先序遍历"></a>先序遍历</h6><h6 id="中序遍历"><a href="#中序遍历" class="headerlink" title="中序遍历"></a>中序遍历</h6><h6 id="后序遍历"><a href="#后序遍历" class="headerlink" title="后序遍历"></a>后序遍历</h6><hr>
<h4 id="红黑树（自平衡的二叉查找树）"><a href="#红黑树（自平衡的二叉查找树）" class="headerlink" title="红黑树（自平衡的二叉查找树）"></a>红黑树（自平衡的二叉查找树）</h4><p><strong>红黑树特点</strong> :</p>
<ol>
<li>每个节点非红即黑；</li>
<li>根节点总是黑色的；</li>
<li>每个叶子节点都是黑色的空节点（NIL节点相当于NULL）；</li>
<li>如果节点是红色的，则它的子节点必须是黑色的（反之不一定）；</li>
<li>从根节点到叶节点或空子节点的每条路径，必须包含相同数目的黑色节点（即相同的黑色高度）。</li>
</ol>
<hr>
<p><strong>红黑树的应用</strong> ：TreeMap、TreeSet以及JDK1.8的HashMap底层都用到了红黑树。</p>
<hr>
<p><strong>为什么要用红黑树？</strong> 简单来说红黑树就是为了解决二叉查找树的缺陷，因为二叉查找树在某些情况下会退化成一个线性结构。</p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230303183830783.png" alt="image-20230303183830783"></p>
<p>依次插入如下五个节点：7,6,5,4,3。依照二叉查找树的特性，结果会变成什么样呢？</p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230303183855366.png" alt="image-20230303183855366"></p>
<h5 id="调整红黑树策略"><a href="#调整红黑树策略" class="headerlink" title="调整红黑树策略"></a>调整红黑树策略</h5><h6 id="变色"><a href="#变色" class="headerlink" title="变色"></a>变色</h6><h6 id="旋转"><a href="#旋转" class="headerlink" title="旋转"></a>旋转</h6><p><strong>左旋转：</strong>待旋转的节点从右边上升到父节点就是左旋。</p>
<p><strong>右旋转：</strong> 待旋转的节点从左边上升到父节点就是右旋</p>
<h5 id="插入操作"><a href="#插入操作" class="headerlink" title="插入操作"></a>插入操作</h5><p>入修复操作分为以下的三种情况，而且新插入的节点的父节点都是红色的：</p>
<ol>
<li>叔叔节点也为红色。</li>
<li>叔叔节点为空，且祖父节点、父节点和新节点处于一条斜线上。</li>
<li>叔叔节点为空，且祖父节点、父节点和新节点不处于一条斜线上。</li>
</ol>
<h6 id="插入操作总结"><a href="#插入操作总结" class="headerlink" title="插入操作总结"></a><strong>插入操作总结</strong></h6><p>插入后的修复操作是一个向root节点回溯的操作，一旦牵涉的节点都符合了红黑树的定义，修复操作结束。之所以会向上回溯是由于case 1操作会将父节点，叔叔节点和祖父节点进行换颜色，有可能会导致祖父节点不平衡(红黑树定义3)。这个时候需要对祖父节点为起点进行调节（向上回溯）。</p>
<p>祖父节点调节后如果还是遇到它的祖父颜色问题，操作就会继续向上回溯，直到root节点为止，根据定义root节点永远是黑色的。在向上的追溯的过程中，针对插入的3中情况进行调节。直到符合红黑树的定义为止。直到牵涉的节点都符合了红黑树的定义，修复操作结束。</p>
<p>如果上面的3中情况如果对应的操作是在右子树上，做对应的镜像操作就是了。</p>
<h5 id="删除操作"><a href="#删除操作" class="headerlink" title="删除操作"></a>删除操作</h5><p>删除操作首先需要做的也是BST的删除操作，删除操作会删除对应的节点，如果是叶子节点就直接删除，如果是非叶子节点，会用对应的中序遍历的后继节点来顶替要删除节点的位置。删除后就需要做删除修复操作，使的树符合红黑树的定义，符合定义的红黑树高度是平衡的。</p>
<p>删除修复操作分为四种情况(删除黑节点后)：</p>
<ol>
<li>待删除的节点的兄弟节点是红色的节点。</li>
<li>待删除的节点的兄弟节点是黑色的节点，且兄弟节点的子节点都是黑色的。</li>
<li>待调整的节点的兄弟节点是黑色的节点，且兄弟节点的左子节点是红色的，右节点是黑色的(兄弟节点在右边)，如果兄弟节点在左边的话，就是兄弟节点的右子节点是红色的，左节点是黑色的。</li>
<li>待调整的节点的兄弟节点是黑色的节点，且右子节点是是红色的(兄弟节点在右边)，如果兄弟节点在左边，则就是对应的就是左节点是红色的。</li>
</ol>
<h6 id="删除操作的总结"><a href="#删除操作的总结" class="headerlink" title="删除操作的总结"></a>删除操作的总结</h6><p>红黑树的删除操作是最复杂的操作，复杂的地方就在于当删除了黑色节点的时候，如何从兄弟节点去借调节点，以保证树的颜色符合定义。由于红色的兄弟节点是没法借调出黑节点的，这样只能通过选择操作让他上升到父节点，而由于它是红节点，所以它的子节点就是黑的，可以借调。</p>
<p>对于兄弟节点是黑色节点的可以分成3种情况来处理，当所以所有的兄弟节点的子节点都是黑色节点时，可以直接将兄弟节点变红，这样局部的红黑树颜色是符合定义的。但是整颗树不一定是符合红黑树定义的，需要往上追溯继续调整。</p>
<p>对于兄弟节点的子节点为左红右黑或者 (全部为红，右红左黑)这两种情况，可以先将前面的情况通过选择转换为后一种情况，在后一种情况下，因为兄弟节点为黑，兄弟节点的右节点为红，可以借调出两个节点出来做黑节点，这样就可以保证删除了黑节点，整棵树还是符合红黑树的定义的，因为黑色节点的个数没有改变。</p>
<p>红黑树的删除操作是遇到删除的节点为红色，或者追溯调整到了root节点，这时删除的修复操作完毕。</p>
<h5 id="总结-6"><a href="#总结-6" class="headerlink" title="总结"></a>总结</h5><p>作为平衡二叉查找树里面众多的实现之一，红黑树无疑是最简洁、实现最为简单的。红黑树通过引入颜色的概念，通过颜色这个约束条件的使用来保持树的高度平衡。作为平衡二叉查找树，旋转是一个必不可少的操作。通过旋转可以降低树的高度，在红黑树里面还可以转换颜色。</p>
<p>红黑树里面的插入和删除的操作比较难理解，这时要注意记住一点：操作之前红黑树是平衡的，颜色是符合定义的。在操作的时候就需要向兄弟节点、父节点、侄子节点借调和互换颜色，要达到这个目的，就需要不断的进行旋转。所以红黑树的插入删除操作需要不停的旋转，一旦借调了别的节点，删除和插入的节点就会达到局部的平衡（局部符合红黑树的定义），但是被借调的节点就不会平衡了，这时就需要以被借调的节点为起点继续进行调整，直到整棵树都是平衡的。在整个修复的过程中，插入具体的分为3种情况，删除分为4种情况。</p>
<p>整个红黑树的查找，插入和删除都是O(logN)的，原因就是整个红黑树的高度是logN，查找从根到叶，走过的路径是树的高度，删除和插入操作是从叶到根的，所以经过的路径都是logN。</p>
<hr>
<h4 id="布隆过滤器"><a href="#布隆过滤器" class="headerlink" title="布隆过滤器"></a>布隆过滤器</h4><h5 id="什么是布隆过滤器？"><a href="#什么是布隆过滤器？" class="headerlink" title="什么是布隆过滤器？"></a>什么是布隆过滤器？</h5><p>我们可以把它看作由二进制向量（或者说位数组）和一系列随机映射函数（哈希函数）两部分组成的数据结构。相比于我们平时常用的的 List、Map 、Set 等数据结构，它占用空间更少并且效率更高，但是缺点是其返回的结果是概率性的，而不是非常准确的。理论情况下添加到集合中的元素越多，误报的可能性就越大。并且，存放在布隆过滤器的数据不容易删除。</p>
<p>一个名叫 Bloom 的人提出了一种来检索元素是否在给定大集合中的数据结构，这种数据结构是高效且性能很好的，但缺点是具有一定的错误识别率和删除难度。并且，理论情况下，添加到集合中的元素越多，误报的可能性就越大。</p>
<hr>
<h5 id="布隆过滤器的原理介绍"><a href="#布隆过滤器的原理介绍" class="headerlink" title="布隆过滤器的原理介绍"></a>布隆过滤器的原理介绍</h5><p><strong>当一个元素加入布隆过滤器中的时候，会进行如下操作：</strong></p>
<ol>
<li>使用布隆过滤器中的哈希函数对元素值进行计算，得到哈希值（有几个哈希函数得到几个哈希值）。</li>
<li>根据得到的哈希值，在位数组中把对应下标的值置为 1。</li>
</ol>
<p><strong>当我们需要判断一个元素是否存在于布隆过滤器的时候，会进行如下操作：</strong></p>
<ol>
<li>对给定元素再次进行相同的哈希计算；</li>
<li>得到值之后判断位数组中的每个元素是否都为 1，如果值都为 1，那么说明这个值在布隆过滤器中，如果存在一个值不为 1，说明该元素不在布隆过滤器中。</li>
</ol>
<p><strong>不同的字符串可能哈希出来的位置相同，这种情况我们可以适当增加位数组大小或者调整我们的哈希函数。</strong></p>
<p>综上，我们可以得出：**布隆过滤器说某个元素存在，小概率会误判。布隆过滤器说某个元素不在，那么这个元素一定不在。</p>
<hr>
<h5 id="布隆过滤器使用场景"><a href="#布隆过滤器使用场景" class="headerlink" title="布隆过滤器使用场景"></a>布隆过滤器使用场景</h5><ul>
<li>判断给定数据是否存在：比如判断一个数字是否存在于包含大量数字的数字集中（数字集很大，5 亿以上！）、 防止缓存穿透（判断请求的数据是否有效避免直接绕过缓存请求数据库）等等、邮箱的垃圾邮件过滤、黑名单功能等等。</li>
<li>去重：比如爬给定网址的时候对已经爬取过的 URL 去重。</li>
</ul>
<hr>
<h5 id="编码实战"><a href="#编码实战" class="headerlink" title="编码实战"></a>编码实战</h5><h6 id="通过-Java-编程手动实现布隆过滤器"><a href="#通过-Java-编程手动实现布隆过滤器" class="headerlink" title="通过 Java 编程手动实现布隆过滤器"></a>通过 Java 编程手动实现布隆过滤器</h6><p>我们上面已经说了布隆过滤器的原理，知道了布隆过滤器的原理之后就可以自己手动实现一个了。</p>
<p>如果你想要手动实现一个的话，你需要：</p>
<ol>
<li>一个合适大小的位数组保存数据</li>
<li>几个不同的哈希函数</li>
<li>添加元素到位数组（布隆过滤器）的方法实现</li>
<li>判断给定元素是否存在于位数组（布隆过滤器）的方法实现。</li>
</ol>
<hr>
<h6 id="利用-Google-开源的-Guava-中自带的布隆过滤器"><a href="#利用-Google-开源的-Guava-中自带的布隆过滤器" class="headerlink" title="利用 Google 开源的 Guava 中自带的布隆过滤器"></a>利用 Google 开源的 Guava 中自带的布隆过滤器</h6><p>自己实现的目的主要是为了让自己搞懂布隆过滤器的原理，Guava 中布隆过滤器的实现算是比较权威的，所以实际项目中我们不需要手动实现一个布隆过滤器。</p>
<p><strong>Guava 提供的布隆过滤器的实现还是很不错的（想要详细了解的可以看一下它的源码实现），但是它有一个重大的缺陷就是只能单机使用（另外，容量扩展也不容易），而现在互联网一般都是分布式的场景。为了解决这个问题，我们就需要用到 Redis 中的布隆过滤器了。</strong></p>
<hr>
<h5 id="Redis-中的布隆过滤器"><a href="#Redis-中的布隆过滤器" class="headerlink" title="Redis 中的布隆过滤器"></a>Redis 中的布隆过滤器</h5><h6 id="使用-Docker-安装"><a href="#使用-Docker-安装" class="headerlink" title="使用 Docker 安装"></a>使用 Docker 安装</h6><h6 id="常用命令一览"><a href="#常用命令一览" class="headerlink" title="常用命令一览"></a>常用命令一览</h6><ul>
<li>**<code>BF.ADD</code>**：将元素添加到布隆过滤器中，如果该过滤器尚不存在，则创建该过滤器。格式：<code>BF.ADD &#123;key&#125; &#123;item&#125;</code>。</li>
<li><strong><code>BF.MADD</code></strong> : 将一个或多个元素添加到“布隆过滤器”中，并创建一个尚不存在的过滤器。该命令的操作方式<code>BF.ADD</code>与之相同，只不过它允许多个输入并返回多个值。格式：<code>BF.MADD &#123;key&#125; &#123;item&#125; [item ...]</code> 。</li>
<li><strong><code>BF.EXISTS</code></strong> : 确定元素是否在布隆过滤器中存在。格式：<code>BF.EXISTS &#123;key&#125; &#123;item&#125;</code>。</li>
<li><strong><code>BF.MEXISTS</code></strong> ： 确定一个或者多个元素是否在布隆过滤器中存在格式：<code>BF.MEXISTS &#123;key&#125; &#123;item&#125; [item ...]</code>。</li>
</ul>
<hr>
<h2 id="算法"><a href="#算法" class="headerlink" title="算法"></a>算法</h2><h3 id="几道常见的字符串算法题"><a href="#几道常见的字符串算法题" class="headerlink" title="几道常见的字符串算法题"></a>几道常见的字符串算法题</h3><h4 id="KMP-算法"><a href="#KMP-算法" class="headerlink" title="KMP 算法"></a>KMP 算法</h4><p>KMP 算法把字符匹配的时间复杂度缩小到 O(m+n) ,而空间复杂度也只有O(m)。因为“暴力搜索”的方法会反复回溯主串，导致效率低下，而KMP算法可以利用已经部分匹配这个有效信息，保持主串上的指针不回溯，通过修改子串的指针，让模式串尽量地移动到有效的位置。</p>
<p><strong>除此之外，再来了解一下BM算法！</strong></p>
<blockquote>
<p>BM算法也是一种精确字符串匹配算法，它采用从右向左比较的方法，同时应用到了两种启发式规则，即坏字符规则 和好后缀规则 ，来决定向右跳跃的距离。基本思路就是从右往左进行字符匹配，遇到不匹配的字符后从坏字符表和好后缀表找一个最大的右移值，将模式串右移继续匹配。 《字符串匹配的KMP算法》</p>
</blockquote>
<hr>
<h4 id="替换空格"><a href="#替换空格" class="headerlink" title="替换空格"></a>替换空格</h4><h4 id="最长公共前缀"><a href="#最长公共前缀" class="headerlink" title="最长公共前缀"></a>最长公共前缀</h4><h4 id="回文串"><a href="#回文串" class="headerlink" title="回文串"></a>回文串</h4><h5 id="最长回文串"><a href="#最长回文串" class="headerlink" title="最长回文串"></a>最长回文串</h5><h5 id="验证回文串"><a href="#验证回文串" class="headerlink" title="验证回文串"></a>验证回文串</h5><h5 id="最长回文子串"><a href="#最长回文子串" class="headerlink" title="最长回文子串"></a>最长回文子串</h5><h5 id="最长回文子序列"><a href="#最长回文子序列" class="headerlink" title="最长回文子序列"></a>最长回文子序列</h5><h4 id="括号匹配深度"><a href="#括号匹配深度" class="headerlink" title="括号匹配深度"></a>括号匹配深度</h4><h4 id="把字符串转换成整数"><a href="#把字符串转换成整数" class="headerlink" title="把字符串转换成整数"></a>把字符串转换成整数</h4><h3 id="几道常见的链表算法题"><a href="#几道常见的链表算法题" class="headerlink" title="几道常见的链表算法题"></a>几道常见的链表算法题</h3><h4 id="两数相加"><a href="#两数相加" class="headerlink" title="两数相加"></a>两数相加</h4><h4 id="翻转链表"><a href="#翻转链表" class="headerlink" title="翻转链表"></a>翻转链表</h4><h4 id="链表中倒数第k个节点"><a href="#链表中倒数第k个节点" class="headerlink" title="链表中倒数第k个节点"></a>链表中倒数第k个节点</h4><h4 id="删除链表的倒数第N个节点"><a href="#删除链表的倒数第N个节点" class="headerlink" title="删除链表的倒数第N个节点"></a>删除链表的倒数第N个节点</h4><h3 id="合剑指offer部分编程题并两个排序的链表"><a href="#合剑指offer部分编程题并两个排序的链表" class="headerlink" title="合剑指offer部分编程题并两个排序的链表"></a>合剑指offer部分编程题并两个排序的链表</h3><h4 id="斐波那契数列"><a href="#斐波那契数列" class="headerlink" title="斐波那契数列"></a>斐波那契数列</h4><h4 id="跳台阶问题跳台阶问题"><a href="#跳台阶问题跳台阶问题" class="headerlink" title="跳台阶问题跳台阶问题"></a>跳台阶问题跳台阶问题</h4><h4 id="变态跳台阶问题"><a href="#变态跳台阶问题" class="headerlink" title="变态跳台阶问题"></a>变态跳台阶问题</h4><h4 id="二维数组查找"><a href="#二维数组查找" class="headerlink" title="二维数组查找"></a>二维数组查找</h4><h4 id="替换空格-1"><a href="#替换空格-1" class="headerlink" title="替换空格"></a>替换空格</h4><h4 id="数值的整数次方"><a href="#数值的整数次方" class="headerlink" title="数值的整数次方"></a>数值的整数次方</h4><h4 id="调整数组顺序使奇数位于偶数前面"><a href="#调整数组顺序使奇数位于偶数前面" class="headerlink" title="调整数组顺序使奇数位于偶数前面"></a>调整数组顺序使奇数位于偶数前面</h4><h4 id="链表中倒数第k个节点-1"><a href="#链表中倒数第k个节点-1" class="headerlink" title="链表中倒数第k个节点"></a>链表中倒数第k个节点</h4><h4 id="用两个栈实现队列"><a href="#用两个栈实现队列" class="headerlink" title="用两个栈实现队列"></a>用两个栈实现队列</h4><h4 id="栈的压入-弹出序列"><a href="#栈的压入-弹出序列" class="headerlink" title="栈的压入,弹出序列"></a>栈的压入,弹出序列</h4><h3 id="十大经典排序算法总结"><a href="#十大经典排序算法总结" class="headerlink" title="十大经典排序算法总结"></a>十大经典排序算法总结</h3><h4 id="简介-3"><a href="#简介-3" class="headerlink" title="简介"></a>简介</h4><p>排序算法可以分为：</p>
<ul>
<li><strong>内部排序</strong> ：数据记录在内存中进行排序。</li>
<li><strong><a target="_blank" rel="noopener" href="https://zh.wikipedia.org/wiki/%E5%A4%96%E6%8E%92%E5%BA%8F">外部排序open in new window</a></strong> ：因排序的数据很大，一次不能容纳全部的排序记录，在排序过程中需要访问外存。</li>
</ul>
<p>常见的内部排序算法有：<strong>插入排序</strong>、<strong>希尔排序</strong>、<strong>选择排序</strong>、<strong>冒泡排序</strong>、<strong>归并排序</strong>、<strong>快速排序</strong>、<strong>堆排序</strong>、<strong>基数排序</strong>等</p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230305184510960.png" alt="image-20230305184510960"></p>
<p><strong>图片名词解释：</strong></p>
<ul>
<li><strong>n</strong>：数据规模</li>
<li><strong>k</strong>：“桶” 的个数</li>
<li><strong>In-place</strong>：占用常数内存，不占用额外内存</li>
<li><strong>Out-place</strong>：占用额外内存</li>
</ul>
<h5 id="术语说明"><a href="#术语说明" class="headerlink" title="术语说明"></a>术语说明</h5><ul>
<li><strong>稳定</strong>：如果 A 原本在 B 前面，而 A&#x3D;B，排序之后 A 仍然在 B 的前面。</li>
<li><strong>不稳定</strong>：如果 A 原本在 B 的前面，而 A&#x3D;B，排序之后 A 可能会出现在 B 的后面。</li>
<li><strong>内排序</strong>：所有排序操作都在内存中完成。</li>
<li><strong>外排序</strong>：由于数据太大，因此把数据放在磁盘中，而排序通过磁盘和内存的数据传输才能进行。</li>
<li><strong>时间复杂度</strong>： 定性描述一个算法执行所耗费的时间。</li>
<li><strong>空间复杂度</strong>：定性描述一个算法执行所需内存的大小。</li>
</ul>
<hr>
<h5 id="算法分类"><a href="#算法分类" class="headerlink" title="算法分类"></a>算法分类</h5><p>十种常见排序算法可以分类两大类别：<strong>比较类排序</strong>和<strong>非比较类排序</strong>。 </p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230305184719733.png" alt="image-20230305184719733"></p>
<h3 id="冒泡排序-Bubble-Sort"><a href="#冒泡排序-Bubble-Sort" class="headerlink" title="冒泡排序 (Bubble Sort)"></a>冒泡排序 (Bubble Sort)</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">/**</span><br><span class="line"> * 冒泡排序</span><br><span class="line"> * @param arr</span><br><span class="line"> * @return arr</span><br><span class="line"> */</span><br><span class="line">public static int[] bubbleSort(int[] arr) &#123;</span><br><span class="line">    for (int i = 1; i &lt; arr.length; i++) &#123;</span><br><span class="line">        // Set a flag, if true, that means the loop has not been swapped,</span><br><span class="line">        // that is, the sequence has been ordered, the sorting has been completed.</span><br><span class="line">        boolean flag = true;</span><br><span class="line">        for (int j = 0; j &lt; arr.length - i; j++) &#123;</span><br><span class="line">            if (arr[j] &gt; arr[j + 1]) &#123;</span><br><span class="line">                int tmp = arr[j];</span><br><span class="line">                arr[j] = arr[j + 1];</span><br><span class="line">                arr[j + 1] = tmp;</span><br><span class="line">			    // Change flag</span><br><span class="line">                flag = false;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        if (flag) &#123;</span><br><span class="line">            break;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    return arr;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h3 id="选择排序-Selection-Sort"><a href="#选择排序-Selection-Sort" class="headerlink" title="选择排序 (Selection Sort)"></a>选择排序 (Selection Sort)</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">/**</span><br><span class="line"> * 选择排序</span><br><span class="line"> * @param arr</span><br><span class="line"> * @return arr</span><br><span class="line"> */</span><br><span class="line">public static int[] selectionSort(int[] arr) &#123;</span><br><span class="line">    for (int i = 0; i &lt; arr.length - 1; i++) &#123;</span><br><span class="line">        int minIndex = i;</span><br><span class="line">        for (int j = i + 1; j &lt; arr.length; j++) &#123;</span><br><span class="line">            if (arr[j] &lt; arr[minIndex]) &#123;</span><br><span class="line">                minIndex = j;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        if (minIndex != i) &#123;</span><br><span class="line">            int tmp = arr[i];</span><br><span class="line">            arr[i] = arr[minIndex];</span><br><span class="line">            arr[minIndex] = tmp;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    return arr;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="插入排序-Insertion-Sort"><a href="#插入排序-Insertion-Sort" class="headerlink" title="插入排序 (Insertion Sort)"></a>插入排序 (Insertion Sort)</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">/**</span><br><span class="line"> * 插入排序</span><br><span class="line"> * @param arr</span><br><span class="line"> * @return arr</span><br><span class="line"> */</span><br><span class="line">public static int[] insertionSort(int[] arr) &#123;</span><br><span class="line">    for (int i = 1; i &lt; arr.length; i++) &#123;</span><br><span class="line">        int preIndex = i - 1;</span><br><span class="line">        int current = arr[i];</span><br><span class="line">        while (preIndex &gt;= 0 &amp;&amp; current &lt; arr[preIndex]) &#123;</span><br><span class="line">            arr[preIndex + 1] = arr[preIndex];</span><br><span class="line">            preIndex -= 1;</span><br><span class="line">        &#125;</span><br><span class="line">        arr[preIndex + 1] = current;</span><br><span class="line">    &#125;</span><br><span class="line">    return arr;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h3 id="希尔排序-Shell-Sort"><a href="#希尔排序-Shell-Sort" class="headerlink" title="希尔排序 (Shell Sort)"></a>希尔排序 (Shell Sort)</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">/**</span><br><span class="line"> * 希尔排序</span><br><span class="line"> *</span><br><span class="line"> * @param arr</span><br><span class="line"> * @return arr</span><br><span class="line"> */</span><br><span class="line">public static int[] shellSort(int[] arr) &#123;</span><br><span class="line">    int n = arr.length;</span><br><span class="line">    int gap = n / 2;</span><br><span class="line">    while (gap &gt; 0) &#123;</span><br><span class="line">        for (int i = gap; i &lt; n; i++) &#123;</span><br><span class="line">            int current = arr[i];</span><br><span class="line">            int preIndex = i - gap;</span><br><span class="line">            // Insertion sort</span><br><span class="line">            while (preIndex &gt;= 0 &amp;&amp; arr[preIndex] &gt; current) &#123;</span><br><span class="line">                arr[preIndex + gap] = arr[preIndex];</span><br><span class="line">                preIndex -= gap;</span><br><span class="line">            &#125;</span><br><span class="line">            arr[preIndex + gap] = current;</span><br><span class="line"></span><br><span class="line">        &#125;</span><br><span class="line">        gap /= 2;</span><br><span class="line">    &#125;</span><br><span class="line">    return arr;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h3 id="归并排序-Merge-Sort"><a href="#归并排序-Merge-Sort" class="headerlink" title="归并排序 (Merge Sort)"></a>归并排序 (Merge Sort)</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line">/**</span><br><span class="line"> * 归并排序</span><br><span class="line"> *</span><br><span class="line"> * @param arr</span><br><span class="line"> * @return arr</span><br><span class="line"> */</span><br><span class="line">public static int[] mergeSort(int[] arr) &#123;</span><br><span class="line">    if (arr.length &lt;= 1) &#123;</span><br><span class="line">        return arr;</span><br><span class="line">    &#125;</span><br><span class="line">    int middle = arr.length / 2;</span><br><span class="line">    int[] arr_1 = Arrays.copyOfRange(arr, 0, middle);</span><br><span class="line">    int[] arr_2 = Arrays.copyOfRange(arr, middle, arr.length);</span><br><span class="line">    return merge(mergeSort(arr_1), mergeSort(arr_2));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * Merge two sorted arrays</span><br><span class="line"> *</span><br><span class="line"> * @param arr_1</span><br><span class="line"> * @param arr_2</span><br><span class="line"> * @return sorted_arr</span><br><span class="line"> */</span><br><span class="line">public static int[] merge(int[] arr_1, int[] arr_2) &#123;</span><br><span class="line">    int[] sorted_arr = new int[arr_1.length + arr_2.length];</span><br><span class="line">    int idx = 0, idx_1 = 0, idx_2 = 0;</span><br><span class="line">    while (idx_1 &lt; arr_1.length &amp;&amp; idx_2 &lt; arr_2.length) &#123;</span><br><span class="line">        if (arr_1[idx_1] &lt; arr_2[idx_2]) &#123;</span><br><span class="line">            sorted_arr[idx] = arr_1[idx_1];</span><br><span class="line">            idx_1 += 1;</span><br><span class="line">        &#125; else &#123;</span><br><span class="line">            sorted_arr[idx] = arr_2[idx_2];</span><br><span class="line">            idx_2 += 1;</span><br><span class="line">        &#125;</span><br><span class="line">        idx += 1;</span><br><span class="line">    &#125;</span><br><span class="line">    if (idx_1 &lt; arr_1.length) &#123;</span><br><span class="line">        while (idx_1 &lt; arr_1.length) &#123;</span><br><span class="line">            sorted_arr[idx] = arr_1[idx_1];</span><br><span class="line">            idx_1 += 1;</span><br><span class="line">            idx += 1;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; else &#123;</span><br><span class="line">        while (idx_2 &lt; arr_2.length) &#123;</span><br><span class="line">            sorted_arr[idx] = arr_2[idx_2];</span><br><span class="line">            idx_2 += 1;</span><br><span class="line">            idx += 1;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    return sorted_arr;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h3 id="快速排序-Quick-Sort"><a href="#快速排序-Quick-Sort" class="headerlink" title="快速排序 (Quick Sort)"></a>快速排序 (Quick Sort)</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">public static int partition(int[] array, int low, int high) &#123;</span><br><span class="line">    int pivot = array[high];</span><br><span class="line">    int pointer = low;</span><br><span class="line">    for (int i = low; i &lt; high; i++) &#123;</span><br><span class="line">        if (array[i] &lt;= pivot) &#123;</span><br><span class="line">            int temp = array[i];</span><br><span class="line">            array[i] = array[pointer];</span><br><span class="line">            array[pointer] = temp;</span><br><span class="line">            pointer++;</span><br><span class="line">        &#125;</span><br><span class="line">        System.out.println(Arrays.toString(array));</span><br><span class="line">    &#125;</span><br><span class="line">    int temp = array[pointer];</span><br><span class="line">    array[pointer] = array[high];</span><br><span class="line">    array[high] = temp;</span><br><span class="line">    return pointer;</span><br><span class="line">&#125;</span><br><span class="line">public static void quickSort(int[] array, int low, int high) &#123;</span><br><span class="line">    if (low &lt; high) &#123;</span><br><span class="line">        int position = partition(array, low, high);</span><br><span class="line">        quickSort(array, low, position - 1);</span><br><span class="line">        quickSort(array, position + 1, high);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="堆排序-Heap-Sort"><a href="#堆排序-Heap-Sort" class="headerlink" title="堆排序 (Heap Sort)"></a>堆排序 (Heap Sort)</h3><p>堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构，并同时满足<strong>堆的性质</strong>：即<strong>子结点的值总是小于（或者大于）它的父节点</strong>。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line">// Global variable that records the length of an array;</span><br><span class="line">static int heapLen;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * Swap the two elements of an array</span><br><span class="line"> * @param arr</span><br><span class="line"> * @param i</span><br><span class="line"> * @param j</span><br><span class="line"> */</span><br><span class="line">private static void swap(int[] arr, int i, int j) &#123;</span><br><span class="line">    int tmp = arr[i];</span><br><span class="line">    arr[i] = arr[j];</span><br><span class="line">    arr[j] = tmp;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * Build Max Heap</span><br><span class="line"> * @param arr</span><br><span class="line"> */</span><br><span class="line">private static void buildMaxHeap(int[] arr) &#123;</span><br><span class="line">    for (int i = arr.length / 2 - 1; i &gt;= 0; i--) &#123;</span><br><span class="line">        heapify(arr, i);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * Adjust it to the maximum heap</span><br><span class="line"> * @param arr</span><br><span class="line"> * @param i</span><br><span class="line"> */</span><br><span class="line">private static void heapify(int[] arr, int i) &#123;</span><br><span class="line">    int left = 2 * i + 1;</span><br><span class="line">    int right = 2 * i + 2;</span><br><span class="line">    int largest = i;</span><br><span class="line">    if (right &lt; heapLen &amp;&amp; arr[right] &gt; arr[largest]) &#123;</span><br><span class="line">        largest = right;</span><br><span class="line">    &#125;</span><br><span class="line">    if (left &lt; heapLen &amp;&amp; arr[left] &gt; arr[largest]) &#123;</span><br><span class="line">        largest = left;</span><br><span class="line">    &#125;</span><br><span class="line">    if (largest != i) &#123;</span><br><span class="line">        swap(arr, largest, i);</span><br><span class="line">        heapify(arr, largest);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * Heap Sort</span><br><span class="line"> * @param arr</span><br><span class="line"> * @return</span><br><span class="line"> */</span><br><span class="line">public static int[] heapSort(int[] arr) &#123;</span><br><span class="line">    // index at the end of the heap</span><br><span class="line">    heapLen = arr.length;</span><br><span class="line">    // build MaxHeap</span><br><span class="line">    buildMaxHeap(arr);</span><br><span class="line">    for (int i = arr.length - 1; i &gt; 0; i--) &#123;</span><br><span class="line">        // Move the top of the heap to the tail of the heap in turn</span><br><span class="line">        swap(arr, 0, i);</span><br><span class="line">        heapLen -= 1;</span><br><span class="line">        heapify(arr, 0);</span><br><span class="line">    &#125;</span><br><span class="line">    return arr;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h3 id="计数排序-Counting-Sort"><a href="#计数排序-Counting-Sort" class="headerlink" title="计数排序 (Counting Sort)"></a>计数排序 (Counting Sort)</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line">/**</span><br><span class="line"> * Gets the maximum and minimum values in the array</span><br><span class="line"> *</span><br><span class="line"> * @param arr</span><br><span class="line"> * @return</span><br><span class="line"> */</span><br><span class="line">private static int[] getMinAndMax(int[] arr) &#123;</span><br><span class="line">    int maxValue = arr[0];</span><br><span class="line">    int minValue = arr[0];</span><br><span class="line">    for (int i = 0; i &lt; arr.length; i++) &#123;</span><br><span class="line">        if (arr[i] &gt; maxValue) &#123;</span><br><span class="line">            maxValue = arr[i];</span><br><span class="line">        &#125; else if (arr[i] &lt; minValue) &#123;</span><br><span class="line">            minValue = arr[i];</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    return new int[] &#123; minValue, maxValue &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * Counting Sort</span><br><span class="line"> *</span><br><span class="line"> * @param arr</span><br><span class="line"> * @return</span><br><span class="line"> */</span><br><span class="line">public static int[] countingSort(int[] arr) &#123;</span><br><span class="line">    if (arr.length &lt; 2) &#123;</span><br><span class="line">        return arr;</span><br><span class="line">    &#125;</span><br><span class="line">    int[] extremum = getMinAndMax(arr);</span><br><span class="line">    int minValue = extremum[0];</span><br><span class="line">    int maxValue = extremum[1];</span><br><span class="line">    int[] countArr = new int[maxValue - minValue + 1];</span><br><span class="line">    int[] result = new int[arr.length];</span><br><span class="line"></span><br><span class="line">    for (int i = 0; i &lt; arr.length; i++) &#123;</span><br><span class="line">        countArr[arr[i] - minValue] += 1;</span><br><span class="line">    &#125;</span><br><span class="line">    for (int i = 1; i &lt; countArr.length; i++) &#123;</span><br><span class="line">        countArr[i] += countArr[i - 1];</span><br><span class="line">    &#125;</span><br><span class="line">    for (int i = arr.length - 1; i &gt;= 0; i--) &#123;</span><br><span class="line">        int idx = countArr[arr[i] - minValue] - 1;</span><br><span class="line">        result[idx] = arr[i];</span><br><span class="line">        countArr[arr[i] - minValue] -= 1;</span><br><span class="line">    &#125;</span><br><span class="line">    return result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h3 id="桶排序-Bucket-Sort"><a href="#桶排序-Bucket-Sort" class="headerlink" title="桶排序 (Bucket Sort)"></a>桶排序 (Bucket Sort)</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line">/**</span><br><span class="line"> * Gets the maximum and minimum values in the array</span><br><span class="line"> * @param arr</span><br><span class="line"> * @return</span><br><span class="line"> */</span><br><span class="line">private static int[] getMinAndMax(List&lt;Integer&gt; arr) &#123;</span><br><span class="line">    int maxValue = arr.get(0);</span><br><span class="line">    int minValue = arr.get(0);</span><br><span class="line">    for (int i : arr) &#123;</span><br><span class="line">        if (i &gt; maxValue) &#123;</span><br><span class="line">            maxValue = i;</span><br><span class="line">        &#125; else if (i &lt; minValue) &#123;</span><br><span class="line">            minValue = i;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    return new int[] &#123; minValue, maxValue &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/**</span><br><span class="line"> * Bucket Sort</span><br><span class="line"> * @param arr</span><br><span class="line"> * @return</span><br><span class="line"> */</span><br><span class="line">public static List&lt;Integer&gt; bucketSort(List&lt;Integer&gt; arr, int bucket_size) &#123;</span><br><span class="line">    if (arr.size() &lt; 2 || bucket_size == 0) &#123;</span><br><span class="line">        return arr;</span><br><span class="line">    &#125;</span><br><span class="line">    int[] extremum = getMinAndMax(arr);</span><br><span class="line">    int minValue = extremum[0];</span><br><span class="line">    int maxValue = extremum[1];</span><br><span class="line">    int bucket_cnt = (maxValue - minValue) / bucket_size + 1;</span><br><span class="line">    List&lt;List&lt;Integer&gt;&gt; buckets = new ArrayList&lt;&gt;();</span><br><span class="line">    for (int i = 0; i &lt; bucket_cnt; i++) &#123;</span><br><span class="line">        buckets.add(new ArrayList&lt;Integer&gt;());</span><br><span class="line">    &#125;</span><br><span class="line">    for (int element : arr) &#123;</span><br><span class="line">        int idx = (element - minValue) / bucket_size;</span><br><span class="line">        buckets.get(idx).add(element);</span><br><span class="line">    &#125;</span><br><span class="line">    for (int i = 0; i &lt; buckets.size(); i++) &#123;</span><br><span class="line">        if (buckets.get(i).size() &gt; 1) &#123;</span><br><span class="line">            buckets.set(i, sort(buckets.get(i), bucket_size / 2));</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    ArrayList&lt;Integer&gt; result = new ArrayList&lt;&gt;();</span><br><span class="line">    for (List&lt;Integer&gt; bucket : buckets) &#123;</span><br><span class="line">        for (int element : bucket) &#123;</span><br><span class="line">            result.add(element);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    return result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h3 id="基数排序-Radix-Sort"><a href="#基数排序-Radix-Sort" class="headerlink" title="基数排序 (Radix Sort)"></a>基数排序 (Radix Sort)</h3><p>基数排序也是非比较的排序算法，对元素中的每一位数字进行排序，从最低位开始排序，复杂度为 <code>O(n×k)</code>，<code>n</code> 为数组长度，<code>k</code> 为数组中元素的最大的位数；</p>
<p>基数排序是按照低位先排序，然后收集；再按照高位排序，然后再收集；依次类推，直到最高位。有时候有些属性是有优先级顺序的，先按低优先级排序，再按高优先级排序。最后的次序就是高优先级高的在前，高优先级相同的低优先级高的在前。基数排序基于分别排序，分别收集，所以是稳定的。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line">/**</span><br><span class="line"> * Radix Sort</span><br><span class="line"> *</span><br><span class="line"> * @param arr</span><br><span class="line"> * @return</span><br><span class="line"> */</span><br><span class="line">public static int[] radixSort(int[] arr) &#123;</span><br><span class="line">    if (arr.length &lt; 2) &#123;</span><br><span class="line">        return arr;</span><br><span class="line">    &#125;</span><br><span class="line">    int N = 1;</span><br><span class="line">    int maxValue = arr[0];</span><br><span class="line">    for (int element : arr) &#123;</span><br><span class="line">        if (element &gt; maxValue) &#123;</span><br><span class="line">            maxValue = element;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    while (maxValue / 10 != 0) &#123;</span><br><span class="line">        maxValue = maxValue / 10;</span><br><span class="line">        N += 1;</span><br><span class="line">    &#125;</span><br><span class="line">    for (int i = 0; i &lt; N; i++) &#123;</span><br><span class="line">        List&lt;List&lt;Integer&gt;&gt; radix = new ArrayList&lt;&gt;();</span><br><span class="line">        for (int k = 0; k &lt; 10; k++) &#123;</span><br><span class="line">            radix.add(new ArrayList&lt;Integer&gt;());</span><br><span class="line">        &#125;</span><br><span class="line">        for (int element : arr) &#123;</span><br><span class="line">            int idx = (element / (int) Math.pow(10, i)) % 10;</span><br><span class="line">            radix.get(idx).add(element);</span><br><span class="line">        &#125;</span><br><span class="line">        int idx = 0;</span><br><span class="line">        for (List&lt;Integer&gt; l : radix) &#123;</span><br><span class="line">            for (int n : l) &#123;</span><br><span class="line">                arr[idx++] = n;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    return arr;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h1 id="数据库"><a href="#数据库" class="headerlink" title="数据库"></a>数据库</h1><h2 id="基础"><a href="#基础" class="headerlink" title="基础"></a>基础</h2><h3 id="数据库基础知识总结"><a href="#数据库基础知识总结" class="headerlink" title="数据库基础知识总结"></a>数据库基础知识总结</h3><h4 id="什么是数据库-数据库管理系统-数据库系统-数据库管理员"><a href="#什么是数据库-数据库管理系统-数据库系统-数据库管理员" class="headerlink" title="什么是数据库, 数据库管理系统, 数据库系统, 数据库管理员?"></a>什么是数据库, 数据库管理系统, 数据库系统, 数据库管理员?</h4><ul>
<li><strong>数据库</strong> : 数据库(DataBase 简称 DB)就是信息的集合或者说数据库是由数据库管理系统管理的数据的集合。</li>
<li><strong>数据库管理系统</strong> : 数据库管理系统(Database Management System 简称 DBMS)是一种操纵和管理数据库的大型软件，通常用于建立、使用和维护数据库。</li>
<li><strong>数据库系统</strong> : 数据库系统(Data Base System，简称 DBS)通常由软件、数据库和数据管理员(DBA)组成。</li>
<li><strong>数据库管理员</strong> : 数据库管理员(Database Administrator, 简称 DBA)负责全面管理和控制数据库系统。</li>
</ul>
<hr>
<h4 id="什么是元组-码-候选码-主码-外码-主属性-非主属性？"><a href="#什么是元组-码-候选码-主码-外码-主属性-非主属性？" class="headerlink" title="什么是元组, 码, 候选码, 主码, 外码, 主属性, 非主属性？"></a>什么是元组, 码, 候选码, 主码, 外码, 主属性, 非主属性？</h4><ul>
<li><strong>元组</strong> ： 元组（tuple）是关系数据库中的基本概念，关系是一张表，表中的每行（即数据库中的每条记录）就是一个元组，每列就是一个属性。 在二维表里，元组也称为行。</li>
<li><strong>码</strong> ：码就是能唯一标识实体的属性，对应表中的列。</li>
<li><strong>候选码</strong> ： 若关系中的某一属性或属性组的值能唯一的标识一个元组，而其任何、子集都不能再标识，则称该属性组为候选码。例如：在学生实体中，“学号”是能唯一的区分学生实体的，同时又假设“姓名”、“班级”的属性组合足以区分学生实体，那么{学号}和{姓名，班级}都是候选码。</li>
<li><strong>主码</strong> : 主码也叫主键。主码是从候选码中选出来的。 一个实体集中只能有一个主码，但可以有多个候选码。</li>
<li><strong>外码</strong> : 外码也叫外键。如果一个关系中的一个属性是另外一个关系中的主码则这个属性为外码。</li>
<li><strong>主属性</strong> ： 候选码中出现过的属性称为主属性。比如关系 工人（工号，身份证号，姓名，性别，部门）. 显然工号和身份证号都能够唯一标示这个关系，所以都是候选码。工号、身份证号这两个属性就是主属性。如果主码是一个属性组，那么属性组中的属性都是主属性。</li>
<li><strong>非主属性：</strong> 不包含在任何一个候选码中的属性称为非主属性。比如在关系——学生（学号，姓名，年龄，性别，班级）中，主码是“学号”，那么其他的“姓名”、“年龄”、“性别”、“班级”就都可以称为非主属性。</li>
</ul>
<hr>
<h4 id="什么是-ER-图？"><a href="#什么是-ER-图？" class="headerlink" title="什么是 ER 图？"></a>什么是 ER 图？</h4><p><strong>ER 图</strong> 全称是 Entity Relationship Diagram（实体联系图），提供了表示实体类型、属性和联系的方法。</p>
<ul>
<li><strong>实体</strong> ：通常是现实世界的业务对象，当然使用一些逻辑对象也可以。比如对于一个校园管理系统，会涉及学生、教师、课程、班级等等实体。在 ER 图中，实体使用矩形框表示。</li>
<li><strong>属性</strong> ：即某个实体拥有的属性，属性用来描述组成实体的要素，对于产品设计来说可以理解为字段。在 ER 图中，属性使用椭圆形表示。</li>
<li><strong>联系</strong> ：即实体与实体之间的关系，这个关系不仅有业务关联关系，还能通过数字表示实体之间的数量对照关系。例如，一个班级会有多个学生就是一种实体间的联系。</li>
</ul>
<hr>
<h4 id="数据库范式了解吗"><a href="#数据库范式了解吗" class="headerlink" title="数据库范式了解吗?"></a>数据库范式了解吗?</h4><ul>
<li>1NF(第一范式)：属性不可再分。</li>
<li>2NF(第二范式)：1NF 的基础之上，消除了非主属性对于码的部分函数依赖。</li>
<li>3NF(第三范式)：3NF 在 2NF 的基础之上，消除了非主属性对于码的传递函数依赖 。</li>
</ul>
<hr>
<h5 id="1NF-第一范式"><a href="#1NF-第一范式" class="headerlink" title="1NF(第一范式)"></a>1NF(第一范式)</h5><p><strong>1NF 是所有关系型数据库的最基本要求</strong> ，也就是说关系型数据库中创建的表一定满足第一范式。</p>
<h5 id="2NF-第二范式"><a href="#2NF-第二范式" class="headerlink" title="2NF(第二范式)"></a>2NF(第二范式)</h5><p>2NF 在 1NF 的基础之上，消除了非主属性对于码的部分函数依赖。如下图所示，展示了第一范式到第二范式的过渡。第二范式在第一范式的基础上增加了一个列，这个列称为主键，非主属性都依赖于主键。</p>
<ul>
<li><strong>函数依赖（functional dependency）</strong> ：若在一张表中，在属性（或属性组）X 的值确定的情况下，必定能确定属性 Y 的值，那么就可以说 Y 函数依赖于 X，写作 X → Y。</li>
<li><strong>部分函数依赖（partial functional dependency）</strong> ：如果 X→Y，并且存在 X 的一个真子集 X0，使得 X0→Y，则称 Y 对 X 部分函数依赖。比如学生基本信息表 R 中（学号，身份证号，姓名）当然学号属性取值是唯一的，在 R 关系中，（学号，身份证号）-&gt;（姓名），（学号）-&gt;（姓名），（身份证号）-&gt;（姓名）；所以姓名部分函数依赖与（学号，身份证号）；</li>
<li><strong>完全函数依赖(Full functional dependency)</strong> ：在一个关系中，若某个非主属性数据项依赖于全部关键字称之为完全函数依赖。比如学生基本信息表 R（学号，班级，姓名）假设不同的班级学号有相同的，班级内学号不能相同，在 R 关系中，（学号，班级）-&gt;（姓名），但是（学号）-&gt;(姓名)不成立，（班级）-&gt;(姓名)不成立，所以姓名完全函数依赖与（学号，班级）；</li>
<li><strong>传递函数依赖</strong> ： 在关系模式 R(U)中，设 X，Y，Z 是 U 的不同的属性子集，如果 X 确定 Y、Y 确定 Z，且有 X 不包含 Y，Y 不确定 X，（X∪Y）∩Z&#x3D;空集合，则称 Z 传递函数依赖(transitive functional dependency) 于 X。传递函数依赖会导致数据冗余和异常。传递函数依赖的 Y 和 Z 子集往往同属于某一个事物，因此可将其合并放到一个表中。比如在关系 R(学号 , 姓名, 系名，系主任)中，学号 → 系名，系名 → 系主任，所以存在非主属性系主任对于学号的传递函数依赖。</li>
</ul>
<hr>
<h5 id="3NF-第三范式"><a href="#3NF-第三范式" class="headerlink" title="3NF(第三范式)"></a>3NF(第三范式)</h5><p>3NF 在 2NF 的基础之上，消除了非主属性对于码的传递函数依赖 。符合 3NF 要求的数据库设计，<strong>基本</strong>上解决了数据冗余过大，插入异常，修改异常，删除异常的问题。比如在关系 R(学号 , 姓名, 系名，系主任)中，学号 → 系名，系名 → 系主任，所以存在非主属性系主任对于学号的传递函数依赖，所以该表的设计，不符合 3NF 的要求。</p>
<hr>
<h4 id="主键和外键有什么区别"><a href="#主键和外键有什么区别" class="headerlink" title="主键和外键有什么区别?"></a>主键和外键有什么区别?</h4><p> <strong>主键(主码)</strong> ：主键用于唯一标识一个元组，不能有重复，不允许为空。一个表只能有一个主键。</p>
<p><strong>外键(外码)</strong> ：外键用来和其他表建立联系用，外键是另一表的主键，外键是可以有重复的，可以是空值。一个表可以有多个外键。</p>
<hr>
<h4 id="为什么不推荐使用外键与级联？"><a href="#为什么不推荐使用外键与级联？" class="headerlink" title="为什么不推荐使用外键与级联？"></a>为什么不推荐使用外键与级联？</h4><blockquote>
<p>【强制】不得使用外键与级联，一切外键概念必须在应用层解决。</p>
<p>说明: 以学生和成绩的关系为例，学生表中的 student_id 是主键，那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id，同时触发成绩表中的 student_id 更新，即为级联更新。外键与级联更新适用于单机低并发，不适合分布式、高并发集群; 级联更新是强阻塞，存在数据库更新风暴的风 险; 外键影响数据库的插入速度</p>
</blockquote>
<ul>
<li><strong>增加了复杂性：</strong> a. 每次做 DELETE 或者 UPDATE 都必须考虑外键约束，会导致开发的时候很痛苦, 测试数据极为不方便; b. 外键的主从关系是定的，假如那天需求有变化，数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。</li>
<li><strong>增加了额外工作</strong>： 数据库需要增加维护外键的工作，比如当我们做一些涉及外键字段的增，删，更新操作之后，需要触发相关操作去检查，保证数据的的一致性和正确性，这样会不得不消耗资源；（个人觉得这个不是不用外键的原因，因为即使你不使用外键，你在应用层面也还是要保证的。所以，我觉得这个影响可以忽略不计。）</li>
<li><strong>对分库分表不友好</strong> ：因为分库分表下外键是无法生效的。</li>
</ul>
<p>上面这种回答不是特别的全面，只是说了外键存在的一个常见的问题。实际上，我们知道外键也是有很多好处的，比如：</p>
<ol>
<li>保证了数据库数据的一致性和完整性；</li>
<li>级联操作方便，减轻了程序代码量；</li>
</ol>
<hr>
<h4 id="x3D-x3D-什么是存储过程-x3D-x3D"><a href="#x3D-x3D-什么是存储过程-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;什么是存储过程?&#x3D;&#x3D;"></a>&#x3D;&#x3D;什么是存储过程?&#x3D;&#x3D;</h4><p>我们可以把存储过程看成是一些 SQL 语句的集合，中间加了点逻辑控制语句。存储过程在业务比较复杂的时候是非常实用的，比如很多时候我们完成一个操作可能需要写一大串 SQL 语句，这时候我们就可以写有一个存储过程，这样也方便了我们下一次的调用。存储过程一旦调试完成通过后就能稳定运行，另外，使用存储过程比单纯 SQL 语句执行要快，因为存储过程是预编译过的。</p>
<p>存储过程在互联网公司应用不多，因为存储过程难以调试和扩展，而且没有移植性，还会消耗数据库资源。</p>
<hr>
<h4 id="drop、delete-与-truncate-区别？"><a href="#drop、delete-与-truncate-区别？" class="headerlink" title="drop、delete 与 truncate 区别？"></a>drop、delete 与 truncate 区别？</h4><p><code>truncate</code> 和不带 where&#96;&#96;子句的 <code>delete</code>、以及 <code>drop</code> 都会删除表内的数据，但是 <strong><code>truncate</code> 和 <code>delete</code> 只删除数据不删除表的结构(定义)，执行 <code>drop</code> 语句，此表的结构也会删除，也就是执行 <code>drop</code> 之后对应的表不复存在</strong></p>
<hr>
<h5 id="用法不同"><a href="#用法不同" class="headerlink" title="用法不同"></a>用法不同</h5><p>  <strong>DML 语句和 DDL 语句区别：</strong></p>
<ul>
<li>DML 是数据库操作语言（Data Manipulation Language）的缩写，是指对数据库中表记录的操作，主要包括表记录的插入、更新、删除和查询，是开发人员日常使用最频繁的操作。</li>
<li>DDL （Data Definition Language）是数据定义语言的缩写，简单来说，就是对数据库内部的对象进行创建、删除、修改的操作语言。它和 DML 语言的最大区别是 DML 只是对表内部数据的操作，而不涉及到表的定义、结构的修改，更不会涉及到其他对象。DDL 语句更多的被数据库管理员（DBA）所使用，一般的开发人员很少使用。</li>
</ul>
<p>另外，由于<code>select</code>不会对表进行破坏，所以有的地方也会把<code>select</code>单独区分开叫做数据库查询语言 DQL（Data Query Language）。</p>
<hr>
<h5 id="执行速度不同"><a href="#执行速度不同" class="headerlink" title="执行速度不同"></a>执行速度不同</h5><p>一般来说：<code>drop</code> &gt; <code>truncate</code> &gt; <code>delete</code>（这个我没有设计测试过）。</p>
<ul>
<li><code>delete</code>命令执行的时候会产生数据库的<code>binlog</code>日志，而日志记录是需要消耗时间的，但是也有个好处方便数据回滚恢复。</li>
<li><code>truncate</code>命令执行的时候不会产生数据库日志，因此比<code>delete</code>要快。除此之外，还会把表的自增值重置和索引恢复到初始大小等。</li>
<li><code>drop</code>命令会把表占用的空间全部释放掉。</li>
</ul>
<p>Tips：你应该更多地关注在使用场景上，而不是执行效率。</p>
<hr>
<h4 id="数据库设计通常分为哪几步"><a href="#数据库设计通常分为哪几步" class="headerlink" title="数据库设计通常分为哪几步?"></a>数据库设计通常分为哪几步?</h4><ol>
<li><strong>需求分析</strong> : 分析用户的需求，包括数据、功能和性能需求。</li>
<li><strong>概念结构设计</strong> : 主要采用 E-R 模型进行设计，包括画 E-R 图。</li>
<li><strong>逻辑结构设计</strong> : 通过将 E-R 图转换成表，实现从 E-R 模型到关系模型的转换。</li>
<li><strong>物理结构设计</strong> : 主要是为所设计的数据库选择合适的存储结构和存取路径。</li>
<li><strong>数据库实施</strong> : 包括编程、测试和试运行</li>
<li><strong>数据库的运行和维护</strong> : 系统的运行与数据库的日常维护。</li>
</ol>
<hr>
<h3 id="NoSQL基础知识总结"><a href="#NoSQL基础知识总结" class="headerlink" title="NoSQL基础知识总结"></a>NoSQL基础知识总结</h3><h4 id="NoSQL-是什么？"><a href="#NoSQL-是什么？" class="headerlink" title="NoSQL 是什么？"></a>NoSQL 是什么？</h4><p>NoNoSQL（Not Only SQL 的缩写）泛指非关系型的数据库，主要针对的是键值、文档以及图形类型数据存储。并且，NoSQL 数据库天生支持分布式，数据冗余和数据分片等特性，旨在提供可扩展的高可用高性能数据存储解决方案。</p>
<p>NoSQL 数据库代表：HBase 、Cassandra、MongoDB、Redis。</p>
<hr>
<h4 id="SQL-和-NoSQL-有什么区别？"><a href="#SQL-和-NoSQL-有什么区别？" class="headerlink" title="SQL 和 NoSQL 有什么区别？"></a>SQL 和 NoSQL 有什么区别？</h4><p><img src="E:\XxdBlog\source_posts\images\test\image-20230306143320709.png" alt="image-20230306143320709"></p>
<p><strong>灵活性：</strong> NoSQL 数据库通常提供灵活的架构，以实现更快速、更多的迭代开发。灵活的数据模型使 NoSQL 数据库成为半结构化和非结构化数据的理想之选。</p>
<p><strong>可扩展性：</strong> NoSQL 数据库通常被设计为通过使用分布式硬件集群来横向扩展，而不是通过添加昂贵和强大的服务器来纵向扩展。</p>
<p><strong>高性能：</strong> NoSQL 数据库针对特定的数据模型和访问模式进行了优化，这与尝试使用关系数据库完成类似功能相比可实现更高的性能。</p>
<p><strong>强大的功能：</strong> NoSQL 数据库提供功能强大的 API 和数据类型，专门针对其各自的数据模型而构建。</p>
<hr>
<h3 id="NoSQL-数据库有哪些类型？"><a href="#NoSQL-数据库有哪些类型？" class="headerlink" title="NoSQL 数据库有哪些类型？"></a>NoSQL 数据库有哪些类型？</h3><ul>
<li><strong>键值</strong> ：键值数据库是一种较简单的数据库，其中每个项目都包含键和值。这是极为灵活的 NoSQL 数据库类型，因为应用可以完全控制 value 字段中存储的内容，没有任何限制。Redis 和 DynanoDB 是两款非常流行的键值数据库。</li>
<li><strong>文档</strong> ：文档数据库中的数据被存储在类似于 JSON（JavaScript 对象表示法）对象的文档中，非常清晰直观。每个文档包含成对的字段和值。这些值通常可以是各种类型，包括字符串、数字、布尔值、数组或对象等，并且它们的结构通常与开发者在代码中使用的对象保持一致。MongoDB 就是一款非常流行的文档数据库。</li>
<li><strong>图形</strong> ：图形数据库旨在轻松构建和运行与高度连接的数据集一起使用的应用程序。图形数据库的典型使用案例包括社交网络、推荐引擎、欺诈检测和知识图形。Neo4j 和 Giraph 是两款非常流行的图形数据库。</li>
<li><strong>宽列</strong> ：宽列存储数据库非常适合需要存储大量的数据。Cassandra 和 HBase 是两款非常流行的宽列存储数据库。</li>
</ul>
<hr>
<h3 id="字符集详解"><a href="#字符集详解" class="headerlink" title="字符集详解"></a>字符集详解</h3><p>MySQL 字符编码集中有两套 UTF-8 编码实现：**<code>utf8</code>** 和 **<code>utf8mb4</code>**。</p>
<p>如果使用 <strong><code>utf8</code></strong> 的话，存储emoji 符号和一些比较复杂的汉字、繁体字就会出错。</p>
<h4 id="何为字符集？"><a href="#何为字符集？" class="headerlink" title="何为字符集？"></a>何为字符集？</h4><p><strong>计算机只能存储二进制的数据，那英文、汉字、表情等字符应该如何存储呢？</strong></p>
<p>我们要将这些字符和二进制的数据一一对应起来，比如说字符“a”对应“01100001”，反之，“01100001”对应 “a”。我们将字符对应二进制数据的过程称为”<strong>字符编码</strong>“，反之，二进制数据解析成字符的过程称为“<strong>字符解码</strong>”。</p>
<hr>
<h4 id="有哪些常见的字符集？"><a href="#有哪些常见的字符集？" class="headerlink" title="有哪些常见的字符集？"></a>有哪些常见的字符集？</h4><p>常见的字符集有 ASCII、GB2312、GBK、UTF-8……。</p>
<p>不同的字符集的主要区别在于：</p>
<ul>
<li>可以表示的字符范围</li>
<li>编码方式</li>
</ul>
<h5 id="ASCII"><a href="#ASCII" class="headerlink" title="ASCII"></a>ASCII</h5><p><strong>ASCII</strong> (<strong>A</strong>merican <strong>S</strong>tandard <strong>C</strong>ode for <strong>I</strong>nformation <strong>I</strong>nterchange，美国信息交换标准代码) 是一套主要用于现代美国英语的字符集（这也是 ASCII 字符集的局限性所在）。</p>
<hr>
<h5 id="GB2312"><a href="#GB2312" class="headerlink" title="GB2312"></a>GB2312</h5><p>GB2312 字符集是一种对汉字比较友好的字符集，共收录 6700 多个汉字，基本涵盖了绝大部分常用汉字。不过，GB2312 字符集不支持绝大部分的生僻字和繁体字。</p>
<h5 id="GBK"><a href="#GBK" class="headerlink" title="GBK"></a>GBK</h5><p>GBK 字符集可以看作是 GB2312 字符集的扩展，兼容 GB2312 字符集，共收录了 20000 多个汉字。</p>
<h5 id="GB18030"><a href="#GB18030" class="headerlink" title="GB18030"></a>GB18030</h5><p>GB18030 完全兼容 GB2312 和 GBK 字符集，纳入中国国内少数民族的文字，且收录了日韩汉字，是目前为止最全面的汉字字符集，共收录汉字 70000 多个。</p>
<h5 id="BIG5"><a href="#BIG5" class="headerlink" title="BIG5"></a>BIG5</h5><p>BIG5 主要针对的是繁体中文，收录了 13000 多个汉字。</p>
<h5 id="Unicode-amp-UTF-8编码"><a href="#Unicode-amp-UTF-8编码" class="headerlink" title="Unicode &amp; UTF-8编码"></a>Unicode &amp; UTF-8编码</h5><p>我们上面也说了不同的字符集可以表示的字符范围以及编码规则存在差异。这就导致了一个非常严重的问题：<strong>使用错误的编码方式查看一个包含字符的文件就会产生乱码现象。</strong></p>
<p>&#x3D;&#x3D;<strong>编码和解码时用了不同或者不兼容的字符集</strong> 。&#x3D;&#x3D;</p>
<p><strong>UTF-8</strong> 是目前使用最广的一种字符编码。UTF-8 可以根据不同的符号自动选择编码的长短，像英文字符只需要 1 个字节就够了，这一点 ASCII 字符集一样 。因此，对于英语字符，UTF-8 编码和 ASCII 码是相同的。</p>
<h4 id="MySQL-字符集"><a href="#MySQL-字符集" class="headerlink" title="MySQL 字符集"></a>MySQL 字符集</h4><p>MySQL 字符编码集中有两套 UTF-8 编码实现：</p>
<ul>
<li><strong><code>utf8</code></strong> ： <code>utf8</code>编码只支持<code>1-3</code>个字节 。 在 <code>utf8</code> 编码中，中文是占 3 个字节，其他数字、英文、符号占一个字节。但 emoji 符号占 4 个字节，一些较复杂的文字、繁体字也是 4 个字节。</li>
<li><strong><code>utf8mb4</code></strong> ： UTF-8 的完整实现，正版！最多支持使用 4 个字节表示字符，因此，可以用来存储 emoji 符号。</li>
</ul>
<p>因此，如果你需要存储<code>emoji</code>类型的数据或者一些比较复杂的文字、繁体字到 MySQL 数据库的话，数据库的编码一定要指定为<code>utf8mb4</code> 而不是<code>utf8</code> ，要不然存储的时候就会报错了。</p>
<hr>
<h3 id="SQL"><a href="#SQL" class="headerlink" title="SQL"></a>SQL</h3><h4 id="SQL语法基础知识总结"><a href="#SQL语法基础知识总结" class="headerlink" title="SQL语法基础知识总结"></a>SQL语法基础知识总结</h4><h5 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h5><h6 id="数据库术语"><a href="#数据库术语" class="headerlink" title="数据库术语"></a>数据库术语</h6><p> <code>数据库（database）</code> - 保存有组织的数据的容器（通常是一个文件或一组文件）。</p>
<p><code>数据表（table）</code> - 某种特定类型数据的结构化清单。</p>
<p><code>模式（schema）</code> - 关于数据库和表的布局及特性的信息。模式定义了数据在表中如何存储，包含存储什么样的数据，数据如何分解，各部分信息如何命名等信息。数据库和表都有模式。</p>
<p><code>列（column）</code> - 表中的一个字段。所有表都是由一个或多个列组成的。</p>
<p><code>行（row）</code> - 表中的一个记录。</p>
<p><code>主键（primary key）</code> - 一列（或一组列），其值能够唯一标识表中每一行。</p>
<hr>
<h6 id="SQL-语法"><a href="#SQL-语法" class="headerlink" title="SQL 语法"></a>SQL 语法</h6><p>SQL 语法结构</p>
<p><strong><code>子句</code></strong> - 是语句和查询的组成成分。（在某些情况下，这些都是可选的。）</p>
<ul>
<li><strong><code>表达式</code></strong> - 可以产生任何标量值，或由列和行的数据库表</li>
<li><strong><code>谓词</code></strong> - 给需要评估的 SQL 三值逻辑（3VL）（true&#x2F;false&#x2F;unknown）或布尔真值指定条件，并限制语句和查询的效果，或改变程序流程。</li>
<li><strong><code>查询</code></strong> - 基于特定条件检索数据。这是 SQL 的一个重要组成部分。</li>
<li><strong><code>语句</code></strong> - 可以持久地影响纲要和数据，也可以控制数据库事务、程序流程、连接、会话或诊断。</li>
</ul>
<hr>
<p>SQL 语法要点</p>
<ul>
<li><strong>SQL 语句不区分大小写</strong>，但是数据库表名、列名和值是否区分，依赖于具体的 DBMS 以及配置。例如：<code>SELECT</code> 与 <code>select</code> 、<code>Select</code> 是相同的。</li>
<li><strong>多条 SQL 语句必须以分号（<code>;</code>）分隔</strong>。</li>
<li>处理 SQL 语句时，<strong>所有空格都被忽略</strong>。</li>
</ul>
<hr>
<h6 id="SQL-分类"><a href="#SQL-分类" class="headerlink" title="SQL 分类"></a>SQL 分类</h6><p><strong>数据定义语言（DDL）</strong></p>
<p>数据定义语言（Data Definition Language，DDL）是 SQL 语言集中负责数据结构定义与数据库对象定义的语言。</p>
<p>DDL 的主要功能是<strong>定义数据库对象</strong>。</p>
<p>DDL 的核心指令是 <code>CREATE</code>、<code>ALTER</code>、<code>DROP</code>。</p>
<hr>
<p><strong>数据操纵语言（DML）</strong></p>
<p>DML 的主要功能是 <strong>访问数据</strong>，因此其语法都是以<strong>读写数据库</strong>为主。</p>
<hr>
<p><strong>事务控制语言（TCL）</strong></p>
<p>事务控制语言 (Transaction Control Language, TCL) 用于<strong>管理数据库中的事务</strong>。这些用于管理由 DML 语句所做的更改。它还允许将语句分组为逻辑事务。</p>
<hr>
<p><strong>数据控制语言（DCL）</strong></p>
<p>DCL 的核心指令是 <code>GRANT</code>、<code>REVOKE</code>。</p>
<p>DCL 以<strong>控制用户的访问权限</strong>为主，因此其指令作法并不复杂，可利用 DCL 控制的权限有：<code>CONNECT</code>、<code>SELECT</code>、<code>INSERT</code>、<code>UPDATE</code>、<code>DELETE</code>、<code>EXECUTE</code>、<code>USAGE</code>、<code>REFERENCES</code>。</p>
<p><strong>我们先来介绍 DML 语句用法。 DML 的主要功能是读写数据库实现增删改查。</strong></p>
<hr>
<h5 id="增删改查"><a href="#增删改查" class="headerlink" title="增删改查"></a>增删改查</h5><h5 id="排序-1"><a href="#排序-1" class="headerlink" title="排序"></a>排序</h5><ul>
<li><code>order by</code> 用于对结果集按照一个列或者多个列进行排序。默认按照升序对记录进行排序，如果需要按照降序对记录进行排序，可以使用 <code>desc</code> 关键字。</li>
<li><code>order by</code> 对多列排序的时候，先排序的列放前面，后排序的列放后面。并且，不同的列可以有不同的排序规则。</li>
</ul>
<hr>
<h5 id="分组"><a href="#分组" class="headerlink" title="分组"></a>分组</h5><p><strong><code>group by</code></strong> ：</p>
<ul>
<li><code>group by</code> 子句将记录分组到汇总行中。</li>
<li><code>group by</code> 为每个组返回一个记录。</li>
<li><code>group by</code> 通常还涉及聚合<code>count</code>，<code>max</code>，<code>sum</code>，<code>avg</code> 等。</li>
<li><code>group by</code> 可以按一列或多列进行分组。</li>
<li><code>group by</code> 按分组字段进行排序后，<code>order by</code> 可以以汇总字段来进行排序。</li>
</ul>
<hr>
<p><strong><code>having</code> vs <code>where</code></strong> ：</p>
<ul>
<li><code>where</code>：过滤过滤指定的行，后面不能加聚合函数（分组函数）。</li>
<li><code>having</code>：过滤分组，必须要与 <code>group by</code> 连用，不能单独使用。</li>
</ul>
<hr>
<h5 id="子查询"><a href="#子查询" class="headerlink" title="子查询"></a>子查询</h5><p>子查询常用在 <code>WHERE</code> 子句和 <code>FROM</code> 子句后边：</p>
<ul>
<li>当用于 <code>WHERE</code> 子句时，根据不同的运算符，子查询可以返回单行单列、多行单列、单行多列数据。子查询就是要返回能够作为 <code>WHERE</code> 子句查询条件的值。</li>
<li>当用于 <code>FROM</code> 子句时，一般返回多行多列数据，相当于返回一张临时表，这样才符合 <code>FROM</code> 后面是表的规则。这种做法能够实现多表联合查询。</li>
</ul>
<p>用于 <code>WHERE</code> 子句的子查询的基本语法如下：</p>
<ul>
<li>子查询需要放在括号<code>( )</code>内。</li>
<li><code>operator</code> 表示用于 where 子句的运算符。</li>
</ul>
<hr>
<h6 id="IN-和-BETWEEN"><a href="#IN-和-BETWEEN" class="headerlink" title="IN 和 BETWEEN"></a>IN 和 BETWEEN</h6><ul>
<li><code>IN</code> 操作符在 <code>WHERE</code> 子句中使用，作用是在指定的几个特定值中任选一个值。</li>
<li><code>BETWEEN</code> 操作符在 <code>WHERE</code> 子句中使用，作用是选取介于某个范围内的值。</li>
</ul>
<hr>
<h6 id="AND、OR、NOT"><a href="#AND、OR、NOT" class="headerlink" title="AND、OR、NOT"></a>AND、OR、NOT</h6><ul>
<li><code>AND</code>、<code>OR</code>、<code>NOT</code> 是用于对过滤条件的逻辑处理指令。</li>
<li><code>AND</code> 优先级高于 <code>OR</code>，为了明确处理顺序，可以使用 <code>()</code>。</li>
<li><code>AND</code> 操作符表示左右条件都要满足。</li>
<li><code>OR</code> 操作符表示左右条件满足任意一个即可。</li>
<li><code>NOT</code> 操作符用于否定一个条件。</li>
</ul>
<hr>
<h6 id="LIKE"><a href="#LIKE" class="headerlink" title="LIKE"></a>LIKE</h6><ul>
<li><code>LIKE</code> 操作符在 <code>WHERE</code> 子句中使用，作用是确定字符串是否匹配模式。</li>
<li>只有字段是文本值时才使用 <code>LIKE</code>。</li>
<li><code>LIKE</code> 支持两个通配符匹配选项：<code>%</code> 和 <code>_</code>。</li>
<li>不要滥用通配符，通配符位于开头处匹配会非常慢。</li>
<li><code>%</code> 表示任何字符出现任意次数。</li>
<li><code>_</code> 表示任何字符出现一次。</li>
</ul>
<hr>
<h5 id="连接"><a href="#连接" class="headerlink" title="连接"></a>连接</h5><p>JOIN 是“连接”的意思，顾名思义，SQL JOIN 子句用于将两个或者多个表联合起来进行查询。</p>
<p><strong>连接表的本质就是将不同表的记录合并起来，形成一张新表。当然，这张新表只是临时的，它仅存在于本次查询期间</strong>。</p>
<h6 id="ON-和-WHERE-的区别："><a href="#ON-和-WHERE-的区别：" class="headerlink" title="ON 和 WHERE 的区别："></a><strong><code>ON</code> 和 <code>WHERE</code> 的区别</strong>：</h6><ul>
<li>连接表时，SQL 会根据连接条件生成一张新的临时表。<code>ON</code> 就是连接条件，它决定临时表的生成。</li>
<li><code>WHERE</code> 是在临时表生成以后，再对临时表中的数据进行过滤，生成最终的结果集，这个时候已经没有 JOIN-ON 了。</li>
</ul>
<p>所以总结来说就是：<strong>SQL 先根据 ON 生成一张临时表，然后再根据 WHERE 对临时表进行筛选</strong>。</p>
<hr>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230306162816462.png" alt="image-20230306162816462"></p>
<p>下图展示了 LEFT JOIN、RIGHT JOIN、INNER JOIN、OUTER JOIN 相关的 7 种用法。</p>
<p><img src="https://img-blog.csdnimg.cn/img_convert/d1794312b448516831369f869814ab39.png" alt="img"></p>
<h5 id="组合"><a href="#组合" class="headerlink" title="组合"></a>组合</h5><p><code>UNION</code> 运算符将两个或更多查询的结果组合起来，并生成一个结果集，其中包含来自 <code>UNION</code> 中参与查询的提取行。</p>
<p><code>UNION</code> 基本规则：</p>
<ul>
<li>所有查询的列数和列顺序必须相同。</li>
<li>每个查询中涉及表的列的数据类型必须相同或兼容。</li>
<li>通常返回的列名取自第一个查询。</li>
</ul>
<p>默认地，<code>UNION</code> 操作符选取不同的值。如果允许重复的值，请使用 <code>UNION ALL</code>。</p>
<hr>
<h5 id="函数"><a href="#函数" class="headerlink" title="函数"></a>函数</h5><p>不同数据库的函数往往各不相同，因此不可移植。本节主要以 MysSQL 的函数为例。</p>
<h6 id="文本处理"><a href="#文本处理" class="headerlink" title="文本处理"></a>文本处理</h6><p><img src="E:\XxdBlog\source_posts\images\test\image-20230306163736832.png" alt="image-20230306163736832"></p>
<h6 id="日期和时间处理"><a href="#日期和时间处理" class="headerlink" title="日期和时间处理"></a>日期和时间处理</h6><ul>
<li>日期格式：<code>YYYY-MM-DD</code></li>
<li>时间格式：<code>HH:MM:SS</code></li>
</ul>
<h6 id="数值处理"><a href="#数值处理" class="headerlink" title="数值处理"></a>数值处理</h6><h5 id="数据定义"><a href="#数据定义" class="headerlink" title="数据定义"></a>数据定义</h5><h6 id="数据库（DATABASE）"><a href="#数据库（DATABASE）" class="headerlink" title="数据库（DATABASE）"></a>数据库（DATABASE）</h6><h6 id="数据表（TABLE）"><a href="#数据表（TABLE）" class="headerlink" title="数据表（TABLE）"></a>数据表（TABLE）</h6><h6 id="视图（VIEW）"><a href="#视图（VIEW）" class="headerlink" title="视图（VIEW）"></a>视图（VIEW）</h6><p>定义：</p>
<ul>
<li>视图是基于 SQL 语句的结果集的可视化的表。</li>
<li>视图是虚拟的表，本身不包含数据，也就不能对其进行索引操作。对视图的操作和对普通表的操作一样。</li>
</ul>
<p>作用：</p>
<ul>
<li>简化复杂的 SQL 操作，比如复杂的联结；</li>
<li>只使用实际表的一部分数据；</li>
<li>通过只给用户访问视图的权限，保证数据的安全性；</li>
<li>更改数据格式和表示。</li>
</ul>
<hr>
<h5 id="索引（INDEX）"><a href="#索引（INDEX）" class="headerlink" title="索引（INDEX）"></a>索引（INDEX）</h5><p><strong>索引是一种用于快速查询和检索数据的数据结构，其本质可以看成是一种排序好的数据结构。</strong></p>
<p><strong>索引是一种用于快速查询和检索数据的数据结构，其本质可以看成是一种排序好的数据结构。</strong></p>
<p>索引的作用就相当于书的目录。打个比方: 我们在查字典的时候，如果没有目录，那我们就只能一页一页的去找我们需要查的那个字，速度很慢。如果有目录了，我们只需要先去目录里查找字的位置，然后直接翻到那一页就行了。</p>
<p><strong>优点</strong> ：</p>
<ul>
<li>使用索引可以大大加快 数据的检索速度（大大减少检索的数据量）, 这也是创建索引的最主要的原因。</li>
<li>通过创建唯一性索引，可以保证数据库表中每一行数据的唯一性。</li>
</ul>
<p><strong>缺点</strong> ：</p>
<ul>
<li>创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候，如果数据有索引，那么索引也需要动态的修改，会降低 SQL 执行效率。</li>
<li>索引需要使用物理文件存储，也会耗费一定空间。</li>
</ul>
<hr>
<h5 id="约束"><a href="#约束" class="headerlink" title="约束"></a>约束</h5><p>约束类型：</p>
<ul>
<li><code>NOT NULL</code> - 指示某列不能存储 NULL 值。</li>
<li><code>UNIQUE</code> - 保证某列的每行必须有唯一的值。</li>
<li><code>PRIMARY KEY</code> - NOT NULL 和 UNIQUE 的结合。确保某列（或两个列多个列的结合）有唯一标识，有助于更容易更快速地找到表中的一个特定的记录。</li>
<li><code>FOREIGN KEY</code> - 保证一个表中的数据匹配另一个表中的值的参照完整性。</li>
<li><code>CHECK</code> - 保证列中的值符合指定的条件。</li>
<li><code>DEFAULT</code> - 规定没有给列赋值时的默认值。</li>
</ul>
<hr>
<h5 id="事务处理"><a href="#事务处理" class="headerlink" title="事务处理"></a>事务处理</h5><p><strong>MySQL 默认是隐式提交</strong>，每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 <code>START TRANSACTION</code> 语句时，会关闭隐式提交；当 <code>COMMIT</code> 或 <code>ROLLBACK</code> 语句执行后，事务会自动关闭，重新恢复隐式提交。</p>
<p>通过 <code>set autocommit=0</code> 可以取消自动提交，直到 <code>set autocommit=1</code> 才会提交；<code>autocommit</code> 标记是针对每个连接而不是针对服务器的。</p>
<p>指令：</p>
<ul>
<li><code>START TRANSACTION</code> - 指令用于标记事务的起始点。</li>
<li><code>SAVEPOINT</code> - 指令用于创建保留点。</li>
<li><code>ROLLBACK TO</code> - 指令用于回滚到指定的保留点；如果没有设置保留点，则回退到 <code>START TRANSACTION</code> 语句处。</li>
<li><code>COMMIT</code> - 提交事务。</li>
</ul>
<p><strong>接下来，我们来介绍 DCL 语句用法。DCL 的主要功能是控制用户的访问权限。</strong></p>
<hr>
<h5 id="权限控制"><a href="#权限控制" class="headerlink" title="权限控制"></a>权限控制</h5><p>要授予用户帐户权限，可以用<code>GRANT</code>命令。有撤销用户的权限，可以用<code>REVOKE</code>命令。</p>
<p>简单解释一下：</p>
<ol>
<li>在<code>GRANT</code>关键字后指定一个或多个权限。如果授予用户多个权限，则每个权限由逗号分隔。</li>
<li><code>ON privilege_level</code> 确定权限应用级别。MySQL 支持 global（<code>*.*</code>），database（<code>database.*</code>），table（<code>database.table</code>）和列级别。如果使用列权限级别，则必须在每个权限之后指定一个或逗号分隔列的列表。</li>
<li><code>user</code> 是要授予权限的用户。如果用户已存在，则<code>GRANT</code>语句将修改其权限。否则，<code>GRANT</code>语句将创建一个新用户。可选子句<code>IDENTIFIED BY</code>允许您为用户设置新的密码。</li>
<li><code>REQUIRE tsl_option</code>指定用户是否必须通过 SSL，X059 等安全连接连接到数据库服务器。</li>
<li>可选 <code>WITH GRANT OPTION</code> 子句允许您授予其他用户或从其他用户中删除您拥有的权限。此外，您可以使用<code>WITH</code>子句分配 MySQL 数据库服务器的资源，例如，设置用户每小时可以使用的连接数或语句数。这在 MySQL 共享托管等共享环境中非常有用。</li>
</ol>
<p>简单解释一下：</p>
<ol>
<li>在 <code>REVOKE</code> 关键字后面指定要从用户撤消的权限列表。您需要用逗号分隔权限。</li>
<li>指定在 <code>ON</code> 子句中撤销特权的特权级别。</li>
<li>指定要撤消 <code>FROM</code> 子句中的权限的用户帐户。</li>
</ol>
<p><code>GRANT</code> 和 <code>REVOKE</code> 可在几个层次上控制访问权限：</p>
<ul>
<li>整个服务器，使用 <code>GRANT ALL</code> 和 <code>REVOKE ALL</code>；</li>
<li>整个数据库，使用 <code>ON database.*</code>；</li>
<li>特定的表，使用 <code>ON database.table</code>；</li>
<li>特定的列；</li>
<li>特定的存储过程。</li>
</ul>
<hr>
<h5 id="存储过程"><a href="#存储过程" class="headerlink" title="存储过程"></a>存储过程</h5><p>使用存储过程的好处：</p>
<ul>
<li>代码封装，保证了一定的安全性；</li>
<li>代码复用；</li>
<li>由于是预先编译，因此具有很高的性能。</li>
</ul>
<p>创建存储过程：</p>
<ul>
<li>命令行中创建存储过程需要自定义分隔符，因为命令行是以 <code>;</code> 为结束符，而存储过程中也包含了分号，因此会错误把这部分分号当成是结束符，造成语法错误。</li>
<li>包含 <code>in</code>、<code>out</code> 和 <code>inout</code> 三种参数。</li>
<li>给变量赋值都需要用 <code>select into</code> 语句。</li>
<li>每次只能给一个变量赋值，不支持集合的操作。</li>
</ul>
<hr>
<h5 id="游标"><a href="#游标" class="headerlink" title="游标"></a>游标</h5><p>游标（cursor）是一个存储在 DBMS 服务器上的数据库查询，它不是一条 <code>SELECT</code> 语句，而是被该语句检索出来的结果集。</p>
<p>在存储过程中使用游标可以对一个结果集进行移动遍历。</p>
<p>游标主要用于交互式应用，其中用户需要滚动屏幕上的数据，并对数据进行浏览或做出更改。</p>
<p>使用游标的几个明确步骤：</p>
<ul>
<li><p>在使用游标前，必须声明(定义)它。这个过程实际上没有检索数据， 它只是定义要使用的 <code>SELECT</code> 语句和游标选项。</p>
</li>
<li><p>一旦声明，就必须打开游标以供使用。这个过程用前面定义的 SELECT 语句把数据实际检索出来。</p>
</li>
<li><p>对于填有数据的游标，根据需要取出(检索)各行。</p>
</li>
<li><p>在结束游标使用时，必须关闭游标，可能的话，释放游标(有赖于具</p>
<p>体的 DBMS)。</p>
</li>
</ul>
<hr>
<h5 id="触发器"><a href="#触发器" class="headerlink" title="触发器"></a>触发器</h5><p>在 MySQL 5.7.2 版之前，可以为每个表定义最多六个触发器。</p>
<ul>
<li><code>BEFORE INSERT</code> - 在将数据插入表格之前激活。</li>
<li><code>AFTER INSERT</code> - 将数据插入表格后激活。</li>
<li><code>BEFORE UPDATE</code> - 在更新表中的数据之前激活。</li>
<li><code>AFTER UPDATE</code> - 更新表中的数据后激活。</li>
<li><code>BEFORE DELETE</code> - 在从表中删除数据之前激活。</li>
<li><code>AFTER DELETE</code> - 从表中删除数据后激活。</li>
</ul>
<hr>
<h4 id="SQL常见面试题总结"><a href="#SQL常见面试题总结" class="headerlink" title="SQL常见面试题总结"></a>SQL常见面试题总结</h4><hr>
<h5 id="检索数据"><a href="#检索数据" class="headerlink" title="检索数据"></a>检索数据</h5><hr>
<h3 id="MySQL"><a href="#MySQL" class="headerlink" title="MySQL"></a>MySQL</h3><h4 id="MySQL常见面试题总结"><a href="#MySQL常见面试题总结" class="headerlink" title="MySQL常见面试题总结"></a>MySQL常见面试题总结</h4><h5 id="MySQL-基础"><a href="#MySQL-基础" class="headerlink" title="MySQL 基础"></a>MySQL 基础</h5><h6 id="什么是关系型数据库？"><a href="#什么是关系型数据库？" class="headerlink" title="什么是关系型数据库？"></a>什么是关系型数据库？</h6><p>顾名思义，关系型数据库（RDBMS，Relational Database Management System）就是一种建立在关系模型的基础上的数据库。关系模型表明了数据库中所存储的数据之间的联系（一对一、一对多、多对多）。</p>
<hr>
<h6 id="什么是-SQL？"><a href="#什么是-SQL？" class="headerlink" title="什么是 SQL？"></a>什么是 SQL？</h6><p>SQL 是一种结构化查询语言(Structured Query Language)，专门用来与数据库打交道，目的是提供一种从数据库中读写数据的简单有效的方法。</p>
<p>几乎所有的主流关系数据库都支持 SQL ，适用性非常强。并且，一些非关系型数据库也兼容 SQL 或者使用的是类似于 SQL 的查询语言。</p>
<hr>
<h6 id="什么是-MySQL？"><a href="#什么是-MySQL？" class="headerlink" title="什么是 MySQL？"></a>什么是 MySQL？</h6><p><strong>MySQL 是一种关系型数据库，主要用于持久化存储我们的系统中的一些数据比如用户信息。</strong></p>
<hr>
<h6 id="MySQL-有什么优点？"><a href="#MySQL-有什么优点？" class="headerlink" title="MySQL 有什么优点？"></a>MySQL 有什么优点？</h6><hr>
<h5 id="MySQL-基础架构"><a href="#MySQL-基础架构" class="headerlink" title="MySQL 基础架构"></a>MySQL 基础架构</h5><p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/13526879-3037b144ed09eb88.png" alt="img"></p>
<p>从上图可以看出， MySQL 主要由下面几部分构成：</p>
<ul>
<li><strong>连接器：</strong> 身份认证和权限相关(登录 MySQL 的时候)。</li>
<li><strong>查询缓存：</strong> 执行查询语句的时候，会先查询缓存（MySQL 8.0 版本后移除，因为这个功能不太实用）。</li>
<li><strong>分析器：</strong> 没有命中缓存的话，SQL 语句就会经过分析器，分析器说白了就是要先看你的 SQL 语句要干嘛，再检查你的 SQL 语句语法是否正确。</li>
<li><strong>优化器：</strong> 按照 MySQL 认为最优的方案去执行。</li>
<li><strong>执行器：</strong> 执行语句，然后从存储引擎返回数据。 执行语句之前会先判断是否有权限，如果没有权限的话，就会报错。</li>
<li><strong>插件式存储引擎</strong> ： 主要负责数据的存储和读取，采用的是插件式架构，支持 InnoDB、MyISAM、Memory 等多种存储引擎。</li>
</ul>
<hr>
<h5 id="MySQL-存储引擎"><a href="#MySQL-存储引擎" class="headerlink" title="MySQL 存储引擎"></a>MySQL 存储引擎</h5><h6 id="MySQL-支持哪些存储引擎？默认使用哪个？"><a href="#MySQL-支持哪些存储引擎？默认使用哪个？" class="headerlink" title="MySQL 支持哪些存储引擎？默认使用哪个？"></a>MySQL 支持哪些存储引擎？默认使用哪个？</h6><p> MySQL 当前默认的存储引擎是 InnoDB。并且，所有的存储引擎中只有 InnoDB 是事务性存储引擎，也就是说只有 InnoDB 支持事务。</p>
<h6 id="MySQL-存储引擎架构了解吗？"><a href="#MySQL-存储引擎架构了解吗？" class="headerlink" title="MySQL 存储引擎架构了解吗？"></a>MySQL 存储引擎架构了解吗？</h6><p>MySQL 存储引擎采用的是 <strong>插件式架构</strong> ，支持多种存储引擎，我们甚至可以为不同的数据库表设置不同的存储引擎以适应不同场景的需要。<strong>存储引擎是基于表的，而不是数据库。</strong></p>
<hr>
<h6 id="MyISAM-和-InnoDB-有什么区别？"><a href="#MyISAM-和-InnoDB-有什么区别？" class="headerlink" title="MyISAM 和 InnoDB 有什么区别？"></a>MyISAM 和 InnoDB 有什么区别？</h6><p><strong>1.是否支持行级锁</strong></p>
<p>MyISAM 只有表级锁(table-level locking)，而 InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。</p>
<p>也就说，MyISAM 一锁就是锁住了整张表，这在并发写的情况下是多么滴憨憨啊！这也是为什么 InnoDB 在并发写的时候，性能更牛皮了！</p>
<p><strong>2.是否支持事务</strong></p>
<p>MyISAM 不提供事务支持。</p>
<p>InnoDB 提供事务支持，实现了 SQL 标准定义了四个隔离级别，具有提交(commit)和回滚(rollback)事务的能力。并且，InnoDB 默认使用的 REPEATABLE-READ（可重读）隔离级别是可以解决幻读问题发生的（基于 MVCC 和 Next-Key Lock）。</p>
<p>关于 MySQL 事务的详细介绍，可以看看我写的这篇文章：<a target="_blank" rel="noopener" href="https://javaguide.cn/database/mysql/transaction-isolation-level.html">MySQL 事务隔离级别详解open in new window</a>。</p>
<p><strong>3.是否支持外键</strong></p>
<p>MyISAM 不支持，而 InnoDB 支持。</p>
<p>外键对于维护数据一致性非常有帮助，但是对性能有一定的损耗。因此，通常情况下，我们是不建议在实际生产项目中使用外键的，在业务代码中进行约束即可！</p>
<p>不过，在代码中进行约束的话，对程序员的能力要求更高，具体是否要采用外键还是要根据你的项目实际情况而定。</p>
<p>总结：一般我们也是不建议在数据库层面使用外键的，应用层面可以解决。不过，这样会对数据的一致性造成威胁。具体要不要使用外键还是要根据你的项目来决定。</p>
<p><strong>4.是否支持数据库异常崩溃后的安全恢复</strong></p>
<p>MyISAM 不支持，而 InnoDB 支持。</p>
<p>使用 InnoDB 的数据库在异常崩溃后，数据库重新启动的时候会保证数据库恢复到崩溃前的状态。这个恢复的过程依赖于 <code>redo log</code> 。</p>
<p><strong>5.是否支持 MVCC</strong></p>
<p>MyISAM 不支持，而 InnoDB 支持。</p>
<p>讲真，这个对比有点废话，毕竟 MyISAM 连行级锁都不支持。MVCC 可以看作是行级锁的一个升级，可以有效减少加锁操作，提高性能。</p>
<p><strong>6.索引实现不一样。</strong></p>
<p>虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构，但是两者的实现方式不太一样。</p>
<p>InnoDB 引擎中，其数据文件本身就是索引文件。相比 MyISAM，索引文件和数据文件是分离的，其表数据文件本身就是按 B+Tree 组织的一个索引结构，树的叶节点 data 域保存了完整的数据记录。</p>
<p>详细区别，推荐你看看我写的这篇文章：<a target="_blank" rel="noopener" href="https://javaguide.cn/database/mysql/mysql-index.html">MySQL 索引详解open in new window</a>。</p>
<p><strong>7.性能有差别。</strong></p>
<p>InnoDB 的性能比 MyISAM 更强大，不管是在读写混合模式下还是只读模式下，随着 CPU 核数的增加，InnoDB 的读写能力呈线性增长。MyISAM 因为读写不能并发，它的处理能力跟核数没关系。</p>
<p><strong>总结</strong> ：</p>
<ul>
<li>InnoDB 支持行级别的锁粒度，MyISAM 不支持，只支持表级别的锁粒度。</li>
<li>MyISAM 不提供事务支持。InnoDB 提供事务支持，实现了 SQL 标准定义了四个隔离级别。</li>
<li>MyISAM 不支持外键，而 InnoDB 支持。</li>
<li>MyISAM 不支持 MVVC，而 InnoDB 支持。</li>
<li>虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构，但是两者的实现方式不太一样。</li>
<li>MyISAM 不支持数据库异常崩溃后的安全恢复，而 InnoDB 支持。</li>
<li>InnoDB 的性能比 MyISAM 更强大。</li>
</ul>
<hr>
<h5 id="MySQL-索引"><a href="#MySQL-索引" class="headerlink" title="MySQL 索引"></a>MySQL 索引</h5><h6 id="MySQL-查询缓存"><a href="#MySQL-查询缓存" class="headerlink" title="MySQL 查询缓存"></a>MySQL 查询缓存</h6><p><strong>开启查询缓存后在同样的查询条件以及数据情况下，会直接在缓存中返回结果</strong>。</p>
<p><strong>查询缓存不命中的情况：</strong></p>
<ol>
<li>任何两个查询在任何字符上的不同都会导致缓存不命中。</li>
<li>如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL 库中的系统表，其查询结果也不会被缓存。</li>
<li>缓存建立之后，MySQL 的查询缓存系统会跟踪查询中涉及的每张表，如果这些表（数据或结构）发生变化，那么和这张表相关的所有缓存数据都将失效。</li>
</ol>
<p><strong>缓存虽然能够提升数据库的查询性能，但是缓存同时也带来了额外的开销，每次查询后都要做一次缓存操作，失效后还要销毁。</strong></p>
<hr>
<h5 id="MySQL-日志"><a href="#MySQL-日志" class="headerlink" title="MySQL 日志"></a>MySQL 日志</h5><ul>
<li>错误⽇志（error log） ：对 MySQL 的启动、运⾏、关闭过程进⾏了记 录。 </li>
<li>⼆进制⽇志（binary log） ：主要记 录的是更改数据库数据的 SQL 语 句。 </li>
<li>⼀般查询⽇志（general query log） ：已建⽴连接的客户端发送给 MySQL 服务器的所有 SQL 记录，因 为 SQL 的量⽐较⼤，默认是不开启 的，也不建议开启。 </li>
<li>慢查询⽇志（slow query log） ：执 ⾏时间超过 long_query_time 秒 钟的查询，解决 SQL 慢查询问题的 时候会⽤到。 </li>
<li>事务⽇志(redo log 和 undo log) ： redo log 是重做⽇志，undo log 是回 滚⽇志。 </li>
<li>中继⽇志(relay log) ：relay log 是复 制过程中产⽣的⽇志，很多⽅⾯都跟 binary log 差不多。不过，relay log 针对的是主从复制中的从库。</li>
<li>DDL ⽇志(metadata log) ：DDL 语 句执⾏的元数据操作。</li>
</ul>
<h6 id="慢查询⽇志有什么⽤？"><a href="#慢查询⽇志有什么⽤？" class="headerlink" title="慢查询⽇志有什么⽤？"></a>慢查询⽇志有什么⽤？</h6><p>慢查询⽇志记录了执⾏时间超过 long_q uery_time （默认是 10s）的所有查 询，在我们解决 SQL 慢查询（SQL 执⾏ 时间过⻓）问题的时候经常会⽤到。</p>
<h6 id="binlog-主要记录了什么？"><a href="#binlog-主要记录了什么？" class="headerlink" title="binlog 主要记录了什么？"></a>binlog 主要记录了什么？</h6><p>MySQL binlog(binary log 即⼆进制⽇志 ⽂件) 主要记录了 MySQL 数据库中数据 的所有变化(数据库执⾏的所有 DDL 和 DML 语句)。</p>
<p>binlog 有⼀个⽐较常⻅的应⽤场景就是主 从复制，MySQL 主从复制依赖于 binlog 。另外，常⻅的⼀些同步 MySQL 数据到 其他数据源的⼯具（⽐如 canal）的底层 ⼀般也是依赖 binlog 。</p>
<p>binlog 通过追加的⽅式进⾏写⼊，⼤⼩没 有限制。并且，我们可以通过 max_binl og_size 参数设置每个 binlog ⽂件的最 ⼤容量，当⽂件⼤⼩达到给定值之后，会 ⽣成新的 binlog ⽂件来保存⽇志，不会 出现前⾯写的⽇志被覆盖的情况。</p>
<h6 id="redo-log-如何保证事务的持久-性"><a href="#redo-log-如何保证事务的持久-性" class="headerlink" title="redo log 如何保证事务的持久 性"></a>redo log 如何保证事务的持久 性</h6><p>我们知道 InnoDB 存储引擎是以⻚为单位 来管理存储空间的，我们往 MySQL 插⼊ 的数据最终都是存在于⻚中的，准确点来 说是数据⻚这种类型。为了减少磁盘 IO 开销，还有⼀个叫做 Buffer Pool(缓冲 池) 的区域，存在于内存中。当我们的数 据对应的⻚不存在于 Buffer Pool 中的 话， MySQL 会先将磁盘上的⻚缓存到 Buffer Pool 中，这样后⾯我们直接操作 的就是 Buffer Pool 中的⻚，这样⼤⼤提 ⾼了读写性能。</p>
<p>MySQL InnoDB 引擎使⽤ redo log 来保 证事务的持久性。redo log 主要做的事情 就是记录⻚的修改，⽐如某个⻚⾯某个偏 移量处修改了⼏个字节的值以及具体被修 改的内容是什么。redo log 中的每⼀条记 录包含了表空间号、数据⻚号、偏移量、 具体修改的数据，甚⾄还可能会记录修改 数据的⻓度（取决于 redo log 类型）。</p>
<p>在事务提交时，我们会将 redo log 按照 刷盘策略刷到磁盘上去，这样即使 MySQL 宕机了，重启之后也能恢复未能 写⼊磁盘的数据，从⽽保证事务的持久 性。也就是说，redo log 让 MySQL 具备 了<strong>崩溃回复能⼒</strong>。</p>
<p>redo log 采⽤<strong>循环写</strong>的⽅式进⾏写⼊，⼤ ⼩固定，当写到结尾时，会回到开头循环 写⽇志，会出现前⾯写的⽇志被覆盖的情 况</p>
<h6 id="⻚修改之后为什么不直接刷盘呢？"><a href="#⻚修改之后为什么不直接刷盘呢？" class="headerlink" title="⻚修改之后为什么不直接刷盘呢？"></a>⻚修改之后为什么不直接刷盘呢？</h6><p>最 ⼤的问题就是 <strong>InnoDB ⻚的⼤⼩⼀般为 16KB</strong>，⽽⻚⼜是磁盘和内存交互的基本 单位。这就导致即使我们只修改了⻚中的 ⼏个字节数据，⼀次刷盘操作也需要将 16KB ⼤⼩的⻚整个都刷新到磁盘中。⽽ 且，这些修改的⻚可能并不相邻，也就是 说这还是随机 IO。 采⽤ redo log 的⽅式就可以避免这种性 能问题，因为 redo log 的刷盘性能很 好。⾸先，redo log 的写⼊属于顺序 IO。 其次，⼀⾏ redo log 记录只占⼏⼗ 个字节。 另外，Buffer Pool 中的⻚（脏⻚）在某 些情况下（⽐如 redo log 快写满了）也会进⾏刷盘操作。不过，这⾥的刷盘操作 会合并写⼊，更⾼效地顺序写⼊到磁盘。</p>
<hr>
<h6 id="x3D-x3D-binlog-和-redolog-有什么区别？-x3D-x3D"><a href="#x3D-x3D-binlog-和-redolog-有什么区别？-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;binlog 和 redolog 有什么区别？&#x3D;&#x3D;"></a>&#x3D;&#x3D;binlog 和 redolog 有什么区别？&#x3D;&#x3D;</h6><ul>
<li>binlog 主要⽤于数据库还原，属于数 据级别的数据恢复，主从复制是 binlog 最常⻅的⼀个应⽤场景。 redolog 主要⽤于保证事务的持久 性，属于事务级别的数据恢复。 </li>
<li>redolog 属于 InnoDB 引擎特有的， binlog 属于所有存储引擎共有的，因 为 binlog 是 MySQL 的 Server 层实 现的。 </li>
<li>redolog 属于物理⽇志，主要记录的 是某个⻚的修改。binlog 属于逻辑⽇志，主要记录的是数据库执⾏的所有 DDL 和 DML 语句。 </li>
<li>binlog 通过追加的⽅式进⾏写⼊，⼤⼩没有限制。redo log 采⽤循环写的⽅式进⾏写⼊，⼤⼩固定，当写到结尾时，会回到开头循环写⽇志。</li>
</ul>
<hr>
<h5 id="MySQL-事务"><a href="#MySQL-事务" class="headerlink" title="MySQL 事务"></a>MySQL 事务</h5><h6 id="何谓事务？"><a href="#何谓事务？" class="headerlink" title="何谓事务？"></a>何谓事务？</h6><p><strong>何为事务？</strong> 一言蔽之，<strong>事务是逻辑上的一组操作，要么都执行，要么都不执行。</strong></p>
<h6 id="何谓数据库事务？"><a href="#何谓数据库事务？" class="headerlink" title="何谓数据库事务？"></a>何谓数据库事务？</h6><p>简单来说，数据库事务可以保证多个对数据库的操作（也就是 SQL 语句）构成一个逻辑上的整体。构成这个逻辑上的整体的这些数据库操作遵循：<strong>要么全部执行成功,要么全部不执行</strong> 。</p>
<p>另外，关系型数据库（例如：<code>MySQL</code>、<code>SQL Server</code>、<code>Oracle</code> 等）事务都有 <strong>ACID</strong> 特性：</p>
<ul>
<li><strong>原子性</strong>（<code>Atomicity</code>） ： 事务是最小的执行单位，不允许分割。事务的原子性确保动作要么全部完成，要么完全不起作用；</li>
<li><strong>一致性</strong>（<code>Consistency</code>）： 执行事务前后，数据保持一致，例如转账业务中，无论事务是否成功，转账者和收款人的总额应该是不变的；</li>
<li><strong>隔离性</strong>（<code>Isolation</code>）： 并发访问数据库时，一个用户的事务不被其他事务所干扰，各并发事务之间数据库是独立的；</li>
<li><strong>持久性</strong>（<code>Durability</code>）： 一个事务被提交之后。它对数据库中数据的改变是持久的，即使数据库发生故障也不应该对其有任何影响。</li>
</ul>
<p><strong>只有保证了事务的持久性、原子性、隔离性之后，一致性才能得到保障。也就是说 A、I、D 是手段，C 是目的！</strong></p>
<hr>
<h6 id="并发事务带来了哪些问题"><a href="#并发事务带来了哪些问题" class="headerlink" title="并发事务带来了哪些问题?"></a>并发事务带来了哪些问题?</h6><p><strong>脏读（Dirty read）</strong></p>
<p>一个事务读取数据并且对数据进行了修改，这个修改对其他事务来说是可见的，即使当前事务没有提交。这时另外一个事务读取了这个还未提交的数据，但第一个事务突然回滚，导致数据并没有被提交到数据库，那第二个事务读取到的就是脏数据，这也就是脏读的由来。</p>
<hr>
<p><strong>丢失修改（Lost to modify）</strong></p>
<p>在一个事务读取一个数据时，另外一个事务也访问了该数据，那么在第一个事务中修改了这个数据后，第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失，因此称为丢失修改。</p>
<hr>
<p><strong>不可重复读（Unrepeatable read）</strong></p>
<p>指在一个事务内多次读同一数据。在这个事务还没有结束时，另一个事务也访问该数据。那么，在第一个事务中的两次读数据之间，由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况，因此称为不可重复读。</p>
<hr>
<p><strong>幻读（Phantom read）</strong></p>
<p>幻读与不可重复读类似。它发生在一个事务读取了几行数据，接着另一个并发事务插入了一些数据时。在随后的查询中，第一个事务就会发现多了一些原本不存在的记录，就好像发生了幻觉一样，所以称为幻读。</p>
<hr>
<h6 id="不可重复读和幻读有什么区别？"><a href="#不可重复读和幻读有什么区别？" class="headerlink" title="不可重复读和幻读有什么区别？"></a><strong>不可重复读和幻读有什么区别？</strong></h6><ul>
<li>不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改；</li>
<li>幻读的重点在于记录新增比如多次执行同一条查询语句（DQL）时，发现查到的记录增加了。</li>
</ul>
<p>幻读其实可以看作是不可重复读的一种特殊情况，单独把区分幻读的原因主要是解决幻读和不可重复读的方案不一样。</p>
<hr>
<h6 id="并发事务的控制方式有哪些？"><a href="#并发事务的控制方式有哪些？" class="headerlink" title="并发事务的控制方式有哪些？"></a>并发事务的控制方式有哪些？</h6><p>MySQL 中并发事务的控制方式无非就两种：<strong>锁</strong> 和 <strong>MVCC</strong>。锁可以看作是悲观控制的模式，多版本并发控制（MVCC，Multiversion concurrency control）可以看作是乐观控制的模式。</p>
<ul>
<li><strong>共享锁（S 锁）</strong> ：又称读锁，事务在读取记录的时候获取共享锁，允许多个事务同时获取（锁兼容）。</li>
<li><strong>排他锁（X 锁）</strong> ：又称写锁&#x2F;独占锁，事务在修改记录的时候获取排他锁，不允许多个事务同时获取。如果一个记录已经被加了排他锁，那其他事务不能再对这条记录加任何类型的锁（锁不兼容）。</li>
</ul>
<hr>
<h6 id="SQL-标准定义了哪些事务隔离级别"><a href="#SQL-标准定义了哪些事务隔离级别" class="headerlink" title="SQL 标准定义了哪些事务隔离级别?"></a>SQL 标准定义了哪些事务隔离级别?</h6><ul>
<li><strong>READ-UNCOMMITTED(读取未提交)</strong> ： 最低的隔离级别，允许读取尚未提交的数据变更，可能会导致脏读、幻读或不可重复读。</li>
<li><strong>READ-COMMITTED(读取已提交)</strong> ： 允许读取并发事务已经提交的数据，可以阻止脏读，但是幻读或不可重复读仍有可能发生。</li>
<li><strong>REPEATABLE-READ(可重复读)</strong> ： 对同一字段的多次读取结果都是一致的，除非数据是被本身事务自己所修改，可以阻止脏读和不可重复读，但幻读仍有可能发生。</li>
<li><strong>SERIALIZABLE(可串行化)</strong> ： 最高的隔离级别，完全服从 ACID 的隔离级别。所有的事务依次逐个执行，这样事务之间就完全不可能产生干扰，也就是说，该级别可以防止脏读、不可重复读以及幻读。</li>
</ul>
<hr>
<h6 id="MySQL-的隔离级别是基于锁实现的吗？"><a href="#MySQL-的隔离级别是基于锁实现的吗？" class="headerlink" title="MySQL 的隔离级别是基于锁实现的吗？"></a>MySQL 的隔离级别是基于锁实现的吗？</h6><p>MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。</p>
<p>SERIALIZABLE 隔离级别是通过锁来实现的，READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过， SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制，就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。</p>
<hr>
<h6 id="MySQL-的默认隔离级别是什么"><a href="#MySQL-的默认隔离级别是什么" class="headerlink" title="MySQL 的默认隔离级别是什么?"></a>MySQL 的默认隔离级别是什么?</h6><p> <strong>REPEATABLE-READ（可重读)</strong></p>
<h5 id="MySQL-锁"><a href="#MySQL-锁" class="headerlink" title="MySQL 锁"></a>MySQL 锁</h5><h6 id="表级锁和行级锁了解吗？有什么区别？"><a href="#表级锁和行级锁了解吗？有什么区别？" class="headerlink" title="表级锁和行级锁了解吗？有什么区别？"></a>表级锁和行级锁了解吗？有什么区别？</h6><p><strong>表级锁和行级锁对比</strong> ：</p>
<ul>
<li><strong>表级锁：</strong> MySQL 中锁定粒度最大的一种锁（全局锁除外），是针对非索引字段加的锁，对当前操作的整张表加锁，实现简单，资源消耗也比较少，加锁快，不会出现死锁。不过，触发锁冲突的概率最高，高并发下效率极低。表级锁和存储引擎无关，MyISAM 和 InnoDB 引擎都支持表级锁。</li>
<li><strong>行级锁：</strong> MySQL 中锁定粒度最小的一种锁，是 <strong>针对索引字段加的锁</strong> ，只针对当前操作的行记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小，并发度高，但加锁的开销也最大，加锁慢，会出现死锁。行级锁和存储引擎有关，是在存储引擎层面实现的。</li>
</ul>
<hr>
<h6 id="行级锁的使用有什么注意事项？"><a href="#行级锁的使用有什么注意事项？" class="headerlink" title="行级锁的使用有什么注意事项？"></a>行级锁的使用有什么注意事项？</h6><p>InnoDB 的行锁是针对索引字段加的锁，表级锁是针对非索引字段加的锁。当我们执行 <code>UPDATE</code>、<code>DELETE</code> 语句时，如果 <code>WHERE</code>条件中字段没有命中唯一索引或者索引失效的话，就会导致扫描全表对表中的所有行记录进行加锁。这个在我们日常工作开发中经常会遇到，一定要多多注意！！！</p>
<hr>
<h6 id="InnoDB-有哪几类行锁？"><a href="#InnoDB-有哪几类行锁？" class="headerlink" title="InnoDB 有哪几类行锁？"></a>InnoDB 有哪几类行锁？</h6><ul>
<li><strong>记录锁（Record Lock）</strong> ：也被称为记录锁，属于单个行记录上的锁。</li>
<li><strong>间隙锁（Gap Lock）</strong> ：锁定一个范围，不包括记录本身。</li>
<li><strong>临键锁（Next-Key Lock）</strong> ：Record Lock+Gap Lock，锁定一个范围，包含记录本身，主要目的是为了解决幻读问题（MySQL 事务部分提到过）。记录锁只能锁住已经存在的记录，为了避免插入新记录，需要依赖间隙锁。</li>
</ul>
<p><strong>在 InnoDB 默认的隔离级别 REPEATABLE-READ 下，行锁默认使用的是 Next-Key Lock。但是，如果操作的索引是唯一索引或主键，InnoDB 会对 Next-Key Lock 进行优化，将其降级为 Record Lock，即仅锁住索引本身，而不是范围。</strong></p>
<hr>
<h6 id="共享锁和排他锁呢？"><a href="#共享锁和排他锁呢？" class="headerlink" title="共享锁和排他锁呢？"></a>共享锁和排他锁呢？</h6><hr>
<h6 id="意向锁有什么作用？"><a href="#意向锁有什么作用？" class="headerlink" title="意向锁有什么作用？"></a>意向锁有什么作用？</h6><ul>
<li><strong>意向共享锁（Intention Shared Lock，IS 锁）</strong>：事务有意向对表中的某些记录加共享锁（S 锁），加共享锁前必须先取得该表的 IS 锁。</li>
<li><strong>意向排他锁（Intention Exclusive Lock，IX 锁）</strong>：事务有意向对表中的某些记录加排他锁（X 锁），加排他锁之前必须先取得该表的 IX 锁。</li>
</ul>
<hr>
<h6 id="当前读和快照读有什么区别？"><a href="#当前读和快照读有什么区别？" class="headerlink" title="当前读和快照读有什么区别？"></a>当前读和快照读有什么区别？</h6><p><strong>快照读</strong>（一致性非锁定读）就是单纯的 <code>SELECT</code> 语句，但不包括下面这两类 <code>SELECT</code> 语句：</p>
<p>快照读的情况下，如果读取的记录正在执行 UPDATE&#x2F;DELETE 操作，读取操作不会因此去等待记录上 X 锁的释放，而是会去读取行的一个快照。</p>
<p>只有在事务隔离级别 RC(读取已提交) 和 RR（可重读）下，InnoDB 才会使用一致性非锁定读：</p>
<ul>
<li>在 RC 级别下，对于快照数据，一致性非锁定读总是读取被锁定行的最新一份快照数据。</li>
<li>在 RR 级别下，对于快照数据，一致性非锁定读总是读取本事务开始时的行数据版本。</li>
</ul>
<p>快照读比较适合对于数据一致性要求不是特别高且追求极致性能的业务场景。</p>
<p><strong>当前读</strong> （一致性锁定读）就是给行记录加 X 锁或 S 锁。                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            </p>
<hr>
<h6 id="MySQL-性能优化"><a href="#MySQL-性能优化" class="headerlink" title="MySQL 性能优化"></a>MySQL 性能优化</h6><h6 id="能用-MySQL-直接存储文件（比如图片）吗？"><a href="#能用-MySQL-直接存储文件（比如图片）吗？" class="headerlink" title="能用 MySQL 直接存储文件（比如图片）吗？"></a>能用 MySQL 直接存储文件（比如图片）吗？</h6><p>可以，但是不建议，建议<strong>数据库只存储文件地址信息，文件由文件存储服务负责存储。</strong></p>
<hr>
<h6 id="MySQL-如何存储-IP-地址？"><a href="#MySQL-如何存储-IP-地址？" class="headerlink" title="MySQL 如何存储 IP 地址？"></a>MySQL 如何存储 IP 地址？</h6><p>MySQL 提供了两个方法来处理 ip 地址</p>
<ul>
<li><code>INET_ATON()</code> ： 把 ip 转为无符号整型 (4-8 位)</li>
<li><code>INET_NTOA()</code> :把整型的 ip 转为地址</li>
</ul>
<p>插入数据前，先用 <code>INET_ATON()</code> 把 ip 地址转为整型，显示数据时，使用 <code>INET_NTOA()</code> 把整型的 ip 地址转为地址显示即可。</p>
<hr>
<h6 id="有哪些常见的-SQL-优化手段？"><a href="#有哪些常见的-SQL-优化手段？" class="headerlink" title="有哪些常见的 SQL 优化手段？"></a>有哪些常见的 SQL 优化手段？</h6><hr>
<h4 id="MySQL高性能优化规范建议总结"><a href="#MySQL高性能优化规范建议总结" class="headerlink" title="MySQL高性能优化规范建议总结"></a>MySQL高性能优化规范建议总结</h4><h5 id="数据库基本设计规范"><a href="#数据库基本设计规范" class="headerlink" title="数据库基本设计规范"></a>数据库基本设计规范</h5><h6 id="使用-TIMESTAMP-4-个字节-或-DATETIME-类型-8-个字节-存储时间"><a href="#使用-TIMESTAMP-4-个字节-或-DATETIME-类型-8-个字节-存储时间" class="headerlink" title="使用 TIMESTAMP(4 个字节) 或 DATETIME 类型 (8 个字节) 存储时间"></a>使用 TIMESTAMP(4 个字节) 或 DATETIME 类型 (8 个字节) 存储时间</h6><p>TIMESTAMP 存储的时间范围 1970-01-01 00:00:01 ~ 2038-01-19-03:14:07</p>
<p>TIMESTAMP 占用 4 字节和 INT 相同，但比 INT 可读性高</p>
<p>超出 TIMESTAMP 取值范围的使用 DATETIME 类型存储</p>
<p><strong>经常会有人用字符串存储日期型的数据（不正确的做法）</strong></p>
<ul>
<li>缺点 1：无法用日期函数进行计算和比较</li>
<li>缺点 2：用字符串存储日期要占用更多的空间</li>
</ul>
<hr>
<h5 id="数据库SQL开发规范"><a href="#数据库SQL开发规范" class="headerlink" title="数据库SQL开发规范"></a>数据库SQL开发规范</h5><h6 id="x3D-x3D-避免数据类型的隐式转换-x3D-x3D"><a href="#x3D-x3D-避免数据类型的隐式转换-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;避免数据类型的隐式转换&#x3D;&#x3D;"></a>&#x3D;&#x3D;避免数据类型的隐式转换&#x3D;&#x3D;</h6><p>当操作符与不同类型的操作数一起使用时，会发生类型转换以使操作数兼容。某些转换是隐式发生的。例如，MySQL 会根据需要自动将字符串转换为数字，反之亦然。以下规则描述了比较操作的转换方式：</p>
<ol>
<li>两个参数至少有一个是<code>NULL</code>时，比较的结果也是<code>NULL</code>，特殊的情况是使用<code>&lt;=&gt;</code>对两个<code>NULL</code>做比较时会返回<code>1</code>，这两种情况都不需要做类型转换</li>
<li>两个参数都是字符串，会按照字符串来比较，不做类型转换</li>
<li>两个参数都是整数，按照整数来比较，不做类型转换</li>
<li>十六进制的值和非数字做比较时，会被当做二进制串</li>
<li>有一个参数是<code>TIMESTAMP</code>或<code>DATETIME</code>，并且另外一个参数是常量，常量会被转换为<code>timestamp</code></li>
<li>有一个参数是<code>decimal</code>类型，如果另外一个参数是<code>decimal</code>或者整数，会将整数转换为<code>decimal</code>后进行比较，如果另外一个参数是浮点数，则会把<code>decimal</code>转换为浮点数进行比较</li>
<li><strong>所有其他情况下，两个参数都会被转换为浮点数再进行比较</strong></li>
</ol>
<p><strong>不以数字开头</strong>的字符串都将转换为<code>0</code>。如<code>&#39;abc&#39;</code>、<code>&#39;a123bc&#39;</code>、<code>&#39;abc123&#39;</code>都会转化为<code>0</code>；</p>
<p><strong>以数字开头的</strong>字符串转换时会进行截取，从第一个字符截取到第一个非数字内容为止。比如<code>&#39;123abc&#39;</code>会转换为<code>123</code>，<code>&#39;012abc&#39;</code>会转换为<code>012</code>也就是<code>12</code>，<code>&#39;5.3a66b78c&#39;</code>会转换为<code>5.3</code>，其他同理。</p>
<p>分析和总结</p>
<p>通过上面的测试我们发现 MySQL 使用操作符的一些特性：</p>
<ol>
<li>当操作符<strong>左右两边的数据类型不一致</strong>时，会发生<strong>隐式转换</strong>。</li>
<li>当 where 查询操作符<strong>左边为数值类型</strong>时发生了隐式转换，那么对效率影响不大，但还是不推荐这么做。</li>
<li>当 where 查询操作符<strong>左边为字符类型</strong>时发生了隐式转换，那么会导致索引失效，造成全表扫描效率极低。</li>
<li>字符串转换为数值类型时，非数字开头的字符串会转化为<code>0</code>，以数字开头的字符串会截取从第一个字符到第一个非数字内容为止的值为转化结果。</li>
</ol>
<p>所以，我们在写 SQL 时一定要养成良好的习惯，查询的字段是什么类型，等号右边的条件就写成对应的类型。特别当查询的字段是字符串时，等号右边的条件一定要用引号引起来标明这是一个字符串，否则会造成索引失效触发全表扫描。</p>
<hr>
<h4 id="重要知识点-3"><a href="#重要知识点-3" class="headerlink" title="重要知识点"></a>重要知识点</h4><h5 id="MySQL索引详解"><a href="#MySQL索引详解" class="headerlink" title="MySQL索引详解"></a>MySQL索引详解</h5><h6 id="索引的底层数据结构"><a href="#索引的底层数据结构" class="headerlink" title="索引的底层数据结构"></a>索引的底层数据结构</h6><hr>
<p><strong>B 树&amp; B+树</strong></p>
<p><strong>B 树&amp; B+树两者有何异同呢？</strong></p>
<ul>
<li>B 树的所有节点既存放键(key) 也存放 数据(data)，而 B+树只有叶子节点存放 key 和 data，其他内节点只存放 key。</li>
<li>B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。</li>
<li>B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找，可能还没有到达叶子节点，检索就结束了。而 B+树的检索效率就很稳定了，任何查找都是从根节点到叶子节点的过程，叶子节点的顺序检索很明显。</li>
</ul>
<hr>
<h6 id="索引类型总结"><a href="#索引类型总结" class="headerlink" title="索引类型总结"></a>索引类型总结</h6><p>按照数据结构维度划分：</p>
<ul>
<li>BTree 索引：MySQL 里默认和最常用的索引类型。只有叶子节点存储 value，非叶子节点只有指针和 key。存储引擎 MyISAM 和 InnoDB 实现 BTree 索引都是使用 B+Tree，但二者实现方式不一样（前面已经介绍了）。</li>
<li>哈希索引：类似键值对的形式，一次即可定位。</li>
<li>RTree 索引：一般不会使用，仅支持 geometry 数据类型，优势在于范围查找，效率较低，通常使用搜索引擎如 ElasticSearch 代替。</li>
<li>全文索引：对文本的内容进行分词，进行搜索。目前只有 <code>CHAR</code>、<code>VARCHAR</code> ，<code>TEXT</code> 列上可以创建全文索引。一般不会使用，效率较低，通常使用搜索引擎如 ElasticSearch 代替。</li>
</ul>
<p>按照底层存储方式角度划分：</p>
<ul>
<li>聚簇索引（聚集索引）：<strong>索引结构和数据一起存放的索引</strong>，InnoDB 中的主键索引就属于聚簇索引。</li>
<li>非聚簇索引（非聚集索引）：<strong>索引结构和数据分开存放的索引</strong>，二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎，不管主键还是非主键，使用的都是非聚簇索引。</li>
</ul>
<p><img src="https://img-blog.csdnimg.cn/20210420165326946.png" alt="img"></p>
<h6 id="主键索引"><a href="#主键索引" class="headerlink" title="主键索引"></a>主键索引</h6><p>当没有显示的指定表的主键时，InnoDB 会自动先检查表中是否有唯一索引且不允许存在 null 值的字段，如果有，则选择该字段为默认的主键，否则 InnoDB 将会自动创建一个 6Byte 的自增主键。</p>
<h6 id="二级索引（辅助索引）"><a href="#二级索引（辅助索引）" class="headerlink" title="二级索引（辅助索引）"></a>二级索引（辅助索引）</h6><p><strong>二级索引的叶子节点存储的数据是主键。也就是说，通过二级索引，可以定位主键的位置。</strong></p>
<ul>
<li><strong>唯一索引(Unique Key)</strong> ：唯一索引也是一种约束。<strong>唯一索引的属性列不能出现重复的数据，但是允许数据为 NULL，一张表允许创建多个唯一索引。</strong> 建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性，而不是为了查询效率。</li>
<li><strong>普通索引(Index)</strong> ：<strong>普通索引的唯一作用就是为了快速查询数据，一张表允许创建多个普通索引，并允许数据重复和 NULL。</strong></li>
<li><strong>前缀索引(Prefix)</strong> ：前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引，相比普通索引建立的数据更小， 因为只取前几个字符。</li>
<li><strong>全文索引(Full Text)</strong> ：全文索引主要是为了检索大文本数据中的关键字的信息，是目前搜索引擎数据库使用的一种技术。Mysql5.6 之前只有 MYISAM 引擎支持全文索引，5.6 之后 InnoDB 也支持了全文索引。</li>
</ul>
<p><img src="https://oss.javaguide.cn/github/javaguide/open-source-project/no-cluster-index.png" alt="img"></p>
<hr>
<h6 id="聚簇索引与非聚簇索引"><a href="#聚簇索引与非聚簇索引" class="headerlink" title="聚簇索引与非聚簇索引"></a>聚簇索引与非聚簇索引</h6><p><strong>聚簇索引（聚集索引）</strong></p>
<p><strong>聚簇索引（Clustered Index）即索引结构和数据一起存放的索引，并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引。</strong></p>
<p><strong>优点</strong> ：</p>
<ul>
<li><strong>查询速度非常快</strong> ：聚簇索引的查询速度非常的快，因为整个 B+树本身就是一颗多叉平衡树，叶子节点也都是有序的，定位到索引的节点，就相当于定位到了数据。相比于非聚簇索引， 聚簇索引少了一次读取数据的 IO 操作。</li>
<li><strong>对排序查找和范围查找优化</strong> ：聚簇索引对于主键的排序查找和范围查找速度非常快。</li>
</ul>
<p><strong>缺点</strong> ：</p>
<ul>
<li><strong>依赖于有序的数据</strong> ：因为 B+树是多路平衡树，如果索引的数据不是有序的，那么就需要在插入时排序，如果数据是整型还好，否则类似于字符串或 UUID 这种又长又难比较的数据，插入或查找的速度肯定比较慢。</li>
<li><strong>更新代价大</strong> ： 如果对索引列的数据被修改时，那么对应的索引也将会被修改，而且聚簇索引的叶子节点还存放着数据，修改代价肯定是较大的，所以对于主键索引来说，主键一般都是不可被修改的。</li>
</ul>
<hr>
<p><strong>非聚簇索引（非聚集索引）</strong></p>
<p><strong>非聚簇索引(Non-Clustered Index)即索引结构和数据分开存放的索引，并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎，不管主键还是非主键，使用的都是非聚簇索引。</strong></p>
<p><strong>优点</strong> ：</p>
<p>更新代价比聚簇索引要小 。非聚簇索引的更新代价就没有聚簇索引那么大了，非聚簇索引的叶子节点是不存放数据的</p>
<p><strong>缺点</strong> ：</p>
<ul>
<li><strong>依赖于有序的数据</strong> ：跟聚簇索引一样，非聚簇索引也依赖于有序的数据</li>
<li><strong>可能会二次查询(回表)（辅助索引）</strong> ：这应该是非聚簇索引最大的缺点了。 当查到索引对应的指针或主键后，可能还需要根据指针或主键再到数据文件或表中查询。</li>
</ul>
<p>主键索引本身的 key 就是主键，查到返回就行了。这种情况就称之为覆盖索引了。</p>
<hr>
<h6 id="覆盖索引和联合索引"><a href="#覆盖索引和联合索引" class="headerlink" title="覆盖索引和联合索引"></a>覆盖索引和联合索引</h6><p> <strong>覆盖索引</strong></p>
<p><strong>覆盖索引即需要查询的字段正好是索引的字段，那么直接根据该索引，就可以查到数据了，而无需&#x3D;&#x3D;回表&#x3D;&#x3D;查询。</strong></p>
<p><strong>联合索引</strong></p>
<ul>
<li>最左前缀匹配原则：从左到右依次到查询条件中去匹配，如果查询条件中存在与联合索引中最左侧字段相匹配的字段，则就会使用该字段过滤一批数据，直至联合索引中全部字段匹配完成，或者在执行过程中遇到范围查询（如 **<code>&gt;</code><strong>、</strong><code>&lt;</code>**）才会停止匹配。对于 <strong><code>&gt;=</code><strong>、</strong><code>&lt;=</code><strong>、</strong><code>BETWEEN</code><strong>、</strong><code>like</code></strong> 前缀匹配的范围查询，并不会停止匹配。所以，我们在使用联合索引时，可以将&#x3D;&#x3D;区分度高的字段&#x3D;&#x3D;放在最左边，这也可以过滤更多数据。</li>
</ul>
<hr>
<h6 id="索引下推"><a href="#索引下推" class="headerlink" title="索引下推"></a>索引下推</h6><p>在<strong>非聚簇索引</strong>遍历过程中，对索引中包含的字段先做判断，过滤掉不符合条件的记录，<strong>减少回表次数</strong>。</p>
<ul>
<li>不使用索引条件下推优化时存储引擎通过索引检索到数据，然后返回给MySQL服务器，服务器然后判断数据是否符合条件。</li>
<li>当使用索引条件下推优化时，如果where条件中包含复合索引中一个以上的字段，MySQL服务器将这一部分判断条件传递给存储引擎，然后由存储引擎通过判断索引是否符合MySQL服务器传递的条件，只有当索引符合条件时才会将数据检索出来返回给MySQL服务器。索引条件下推优化可以减少存储引擎查询基础表的次数，也可以减少MySQL服务器从存储引擎接收数据的次数。</li>
</ul>
<hr>
<h6 id="正确使用索引的一些建议"><a href="#正确使用索引的一些建议" class="headerlink" title="正确使用索引的一些建议"></a>正确使用索引的一些建议</h6><p><strong>加索引</strong></p>
<ul>
<li><strong>不为 NULL 的字段</strong> ：索引字段的数据应该尽量不为 NULL，因为对于数据为 NULL 的字段，数据库较难优化。如果字段频繁被查询，但又避免不了为 NULL，建议使用 0,1,true,false 这样语义较为清晰的短值或短字符作为替代。</li>
<li><strong>被频繁查询的字段</strong> ：我们创建索引的字段应该是查询操作非常频繁的字段。</li>
<li><strong>被作为条件查询的字段</strong> ：被作为 WHERE 条件查询的字段，应该被考虑建立索引。</li>
<li><strong>频繁需要排序的字段</strong> ：索引已经排序，这样查询可以利用索引的排序，加快排序查询时间。</li>
<li><strong>被经常频繁用于连接的字段</strong> ：经常用于连接的字段可能是一些外键列，对于外键列并不一定要建立外键，只是说该列涉及到表与表的关系。对于频繁被连接查询的字段，可以考虑建立索引，提高多表连接查询的效率。</li>
</ul>
<p><strong>不加索引</strong></p>
<ul>
<li>被频繁更新的字段应该慎重建立索引</li>
<li>限制每张表上的索引数量</li>
<li>尽可能的考虑建立联合索引而不是单列索引</li>
<li>注意避免冗余索引</li>
<li>字符串类型的字段使用前缀索引代替普通索引</li>
<li>避免索引失效</li>
<li>删除长期未使用的索引</li>
<li>知道如何分析语句是否走索引查询  EXPLAIN</li>
</ul>
<hr>
<h5 id="查询缓存"><a href="#查询缓存" class="headerlink" title="查询缓存"></a>查询缓存</h5><p><strong>开启查询缓存后在同样的查询条件以及数据情况下，会直接在缓存中返回结果</strong>。</p>
<p><strong>查询缓存不命中的情况：</strong></p>
<ol>
<li>任何两个查询在任何字符上的不同都会导致缓存不命中。</li>
<li>如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL 库中的系统表，其查询结果也不会被缓存。</li>
<li>缓存建立之后，MySQL 的查询缓存系统会跟踪查询中涉及的每张表，如果这些表（数据或结构）发生变化，那么和这张表相关的所有缓存数据都将失效。</li>
</ol>
<p><strong>缓存虽然能够提升数据库的查询性能，但是缓存同时也带来了额外的开销，每次查询后都要做一次缓存操作，失效后还要销毁</strong></p>
<p><strong>还可以通过 <code>sql_cache</code> 和 <code>sql_no_cache</code> 来控制某个查询语句是否需要缓存</strong></p>
<hr>
<h5 id="MySQL三大日志-binlog、redo-log和undo-log-详解"><a href="#MySQL三大日志-binlog、redo-log和undo-log-详解" class="headerlink" title="MySQL三大日志(binlog、redo log和undo log)详解"></a>MySQL三大日志(binlog、redo log和undo log)详解</h5><p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/01.png" alt="img"></p>
<h6 id="redo-log（持久性）"><a href="#redo-log（持久性）" class="headerlink" title="redo log（持久性）"></a>redo log（持久性）</h6><p>redo log包括两部分：一个是内存中的**日志缓冲(redo log buffer)<strong>，另一个是磁盘上的</strong>日志文件(redo log file)**。</p>
<p><code>redo log</code>（重做日志）是<code>InnoDB</code>存储引擎独有的，它让<code>MySQL</code>拥有了崩溃恢复能力。利用内存中Buffer Pool实现高速读写，减少IO开销。并将执行的操作对缓冲池进行，将操作记录到redo log buffer中，刷盘到redo log文件中中。</p>
<p><img src="https://oss.javaguide.cn/github/javaguide/03.png" alt="img"></p>
<p><strong>刷盘时机</strong></p>
<p><code>InnoDB</code> 存储引擎为 <code>redo log</code> 的刷盘策略提供了 <code>innodb_flush_log_at_trx_commit</code> 参数，它支持三种策略：</p>
<ul>
<li><strong>0</strong> ：设置为 0 的时候，表示每次事务提交时不进行刷盘操作</li>
<li><strong>1</strong> ：设置为 1 的时候，表示每次事务提交时都将进行刷盘操作（默认值）</li>
<li><strong>2</strong> ：设置为 2 的时候，表示每次事务提交时都只把 redo log buffer 内容写入 page cache</li>
</ul>
<p><code>innodb_flush_log_at_trx_commit</code> 参数默认为 1 ，也就是说当事务提交时会调用 <code>fsync</code> 对 redo log 进行刷盘</p>
<p>另外，<code>InnoDB</code> 存储引擎有一个后台线程，每隔<code>1</code> 秒，就会把 <code>redo log buffer</code> 中的内容写到文件系统缓存（<code>page cache</code>），然后调用 <code>fsync</code> 刷盘。</p>
<p>为<code>0</code>时，如果<code>MySQL</code>挂了或宕机可能会有<code>1</code>秒数据的丢失。</p>
<p>为<code>1</code>时， 只要事务提交成功，<code>redo log</code>记录就一定在硬盘里，不会有任何数据丢失。</p>
<p>为<code>2</code>时， 只要事务提交成功，<code>redo log buffer</code>中的内容只写入文件系统缓存（<code>page cache</code>）。如果仅仅只是<code>MySQL</code>挂了不会有任何数据丢失，但是宕机可能会有<code>1</code>秒数据的丢失。</p>
<hr>
<h6 id="binlog-一致性"><a href="#binlog-一致性" class="headerlink" title="binlog(一致性)"></a>binlog(一致性)</h6><p><code>binlog</code> 是逻辑日志，记录内容是语句的原始逻辑，类似于“给 ID&#x3D;2 这一行的 c 字段加 1”，属于<code>MySQL Server</code> 层。</p>
<p>不管用什么存储引擎，只要发生了表数据更新，都会产生 <code>binlog</code> 日志。</p>
<p>那 <code>binlog</code> 到底是用来干嘛的？</p>
<p>可以说<code>MySQL</code>数据库的<strong>数据备份、主备、主主、主从</strong>都离不开<code>binlog</code>，需要依靠<code>binlog</code>来同步数据，保证数据一致性。</p>
<p><strong>写入机制</strong></p>
<p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/04-20220305234747840.png" alt="img"></p>
<ul>
<li><strong>上图的 write，是指把日志写入到文件系统的 page cache，并没有把数据持久化到磁盘，所以速度比较快</strong></li>
<li><strong>上图的 fsync，才是将数据持久化到磁盘的操作</strong></li>
</ul>
<hr>
<p><strong>两阶段提交</strong></p>
<p>将<code>redo log</code>的写入拆成了两个步骤<code>prepare</code>和<code>commit</code>，这就是<strong>两阶段提交</strong>。</p>
<p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/04-20220305234956774.png" alt="img"></p>
<p>使用<strong>两阶段提交</strong>后，写入<code>binlog</code>时发生异常也不会有影响，因为<code>MySQL</code>根据<code>redo log</code>日志恢复数据时，发现<code>redo log</code>还处于<code>prepare</code>阶段，并且没有对应<code>binlog</code>日志，就会回滚该事务。</p>
<hr>
<h6 id="undo-log-原子性"><a href="#undo-log-原子性" class="headerlink" title="undo log(原子性)"></a>undo log(原子性)</h6><p><code>MVCC</code> 的实现依赖于：<strong>隐藏字段、Read View、undo log</strong>。</p>
<hr>
<h5 id="InnoDB存储引擎对MVCC的实现"><a href="#InnoDB存储引擎对MVCC的实现" class="headerlink" title="InnoDB存储引擎对MVCC的实现"></a>InnoDB存储引擎对MVCC的实现</h5><h6 id="InnoDB-对-MVCC-的实现"><a href="#InnoDB-对-MVCC-的实现" class="headerlink" title="InnoDB 对 MVCC 的实现"></a>InnoDB 对 MVCC 的实现</h6><p><code>MVCC</code> 的实现依赖于：<strong>隐藏字段、Read View、undo log</strong>。滚日志会先于数据持久化到磁盘上。</p>
<p><strong>隐藏字段</strong></p>
<ul>
<li><code>DB_TRX_ID（6字节）</code>：表示最后一次插入或更新该行的事务 id。此外，<code>delete</code> 操作在内部被视为更新，只不过会在记录头 <code>Record header</code> 中的 <code>deleted_flag</code> 字段将其标记为已删除</li>
<li><code>DB_ROLL_PTR（7字节）</code> 回滚指针，指向该行的 <code>undo log</code> 。如果该行未被更新，则为空</li>
<li><code>DB_ROW_ID（6字节）</code>：如果没有设置主键且该表没有唯一非空索引时，<code>InnoDB</code> 会使用该 id 来生成聚簇索引</li>
</ul>
<hr>
<p><strong>ReadView</strong></p>
<p><code>Read View</code>主要是用来做可见性判断，里面保存了 “当前对本事务不可见的其他活跃事务”</p>
<ul>
<li><code>m_low_limit_id</code>：目前出现过的最大的事务 ID+1，即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见</li>
<li><code>m_up_limit_id</code>：活跃事务列表 <code>m_ids</code> 中最小的事务 ID，如果 <code>m_ids</code> 为空，则 <code>m_up_limit_id</code> 为 <code>m_low_limit_id</code>。小于这个 ID 的数据版本均可见</li>
<li><code>m_ids</code>：<code>Read View</code> 创建时其他未提交的活跃事务 ID 列表。创建 <code>Read View</code>时，将当前未提交事务 ID 记录下来，后续即使它们修改了记录行的值，对于当前事务也是不可见的。<code>m_ids</code> 不包括当前事务自己和已提交的事务（正在内存中）</li>
<li><code>m_creator_trx_id</code>：创建该 <code>Read View</code> 的事务 ID</li>
</ul>
<p><img src="https://javaguide.cn/assets/trans_visible.048192c5.png" alt="trans_visible"></p>
<hr>
<p><strong>undo-log</strong></p>
<p><code>undo log</code> 主要有两个作用：</p>
<ul>
<li>当事务回滚时用于将数据恢复到修改前的样子</li>
<li>另一个作用是 <code>MVCC</code> ，当读取记录时，若该记录被其他事务占用或当前版本对该事务不可见，则可以通过 <code>undo log</code> 读取之前的版本数据，以此实现非锁定读</li>
</ul>
<p><strong>在 <code>InnoDB</code> 存储引擎中 <code>undo log</code> 分为两种： <code>insert undo log</code> 和 <code>update undo log</code>：</strong></p>
<ol>
<li><strong><code>insert undo log</code></strong> ：指在 <code>insert</code> 操作中产生的 <code>undo log</code>。因为 <code>insert</code> 操作的记录只对事务本身可见，对其他事务不可见，故该 <code>undo log</code> 可以在事务提交后直接删除。不需要进行 <code>purge</code> 操作</li>
<li><strong><code>update undo log</code></strong> ：<code>update</code> 或 <code>delete</code> 操作中产生的 <code>undo log</code>。该 <code>undo log</code>可能需要提供 <code>MVCC</code> 机制，因此不能在事务提交时就进行删除。提交时放入 <code>undo log</code> 链表，等待 <code>purge线程</code> 进行最后的删除</li>
</ol>
<p><strong>数据第一次被修改时：</strong></p>
<p><img src="https://javaguide.cn/assets/c52ff79f-10e6-46cb-b5d4-3c9cbcc1934a.b60a6e78.png" alt="img"></p>
<p><strong>数据第二次被修改时：</strong></p>
<p><img src="https://javaguide.cn/assets/6a276e7a-b0da-4c7b-bdf7-c0c7b7b3b31c.2e496ea1.png" alt="img"></p>
<p><strong>数据可见性算法</strong></p>
<p>如果记录 DB_TRX_ID &lt; m_up_limit_id，那么表明最新修改该行的事务（DB_TRX_ID）在当前事务创建快照之前就提交了，所以该记录行的值对当前事务是可见的</p>
<p>如果 DB_TRX_ID &gt;&#x3D; m_low_limit_id，那么表明最新修改该行的事务（DB_TRX_ID）在当前事务创建快照之后才修改该行，所以该记录行的值对当前事务不可见。跳到步骤 5</p>
<p>m_ids 为空，则表明在当前事务创建快照之前，修改该行的事务就已经提交了，所以该记录行的值对当前事务是可见的</p>
<p>如果 m_up_limit_id &lt;&#x3D; DB_TRX_ID &lt; m_low_limit_id，表明最新修改该行的事务（DB_TRX_ID）在当前事务创建快照的时候可能处于“活动状态”或者“已提交状态”；所以就要对活跃事务列表 m_ids 进行查找（源码中是用的二分查找，因为是有序的）</p>
<ul>
<li>如果在活跃事务列表 m_ids 中能找到 DB_TRX_ID，表明：① 在当前事务创建快照前，该记录行的值被事务 ID 为 DB_TRX_ID 的事务修改了，但没有提交；或者 ② 在当前事务创建快照后，该记录行的值被事务 ID 为 DB_TRX_ID 的事务修改了。这些情况下，这个记录行的值对当前事务都是不可见的。跳到步骤 5</li>
<li>在活跃事务列表中找不到，则表明“id 为 trx_id 的事务”在修改“该记录行的值”后，在“当前事务”创建快照前就已经提交了，所以记录行对当前事务可见</li>
</ul>
<p>在该记录行的 DB_ROLL_PTR 指针所指向的 <code>undo log</code> 取出快照记录，用快照记录的 DB_TRX_ID 跳到步骤 1 重新开始判断，直到找到满足的快照版本或返回空</p>
<hr>
<h6 id="RC-和-RR-隔离级别下-MVCC-的差异"><a href="#RC-和-RR-隔离级别下-MVCC-的差异" class="headerlink" title="RC 和 RR 隔离级别下 MVCC 的差异"></a>RC 和 RR 隔离级别下 MVCC 的差异</h6><p>在事务隔离级别 <code>RC</code> 和 <code>RR</code> （InnoDB 存储引擎的默认事务隔离级别）下，<code>InnoDB</code> 存储引擎使用 <code>MVCC</code>（非锁定一致性读），但它们生成 <code>Read View</code> 的时机却不同</p>
<ul>
<li>在 RC 隔离级别下的 <strong><code>每次select</code></strong> 查询前都生成一个<code>Read View</code> (m_ids 列表)，导致不可重复读</li>
<li>在 RR 隔离级别下只在事务开始后 <strong><code>第一次select</code></strong> 数据前生成一个<code>Read View</code>（m_ids 列表）</li>
</ul>
<hr>
<h6 id="MVCC-解决不可重复读问题"><a href="#MVCC-解决不可重复读问题" class="headerlink" title="MVCC 解决不可重复读问题"></a>MVCC 解决不可重复读问题</h6><hr>
<h6 id="MVCC➕Next-key-Lock-防止幻读"><a href="#MVCC➕Next-key-Lock-防止幻读" class="headerlink" title="MVCC➕Next-key-Lock 防止幻读"></a>MVCC➕Next-key-Lock 防止幻读</h6><hr>
<h6 id="MVCC-解决不可重复读问题-1"><a href="#MVCC-解决不可重复读问题-1" class="headerlink" title="MVCC 解决不可重复读问题"></a>MVCC 解决不可重复读问题</h6><hr>
<h5 id="SQL语句在MySQL中的执行过程"><a href="#SQL语句在MySQL中的执行过程" class="headerlink" title="SQL语句在MySQL中的执行过程"></a>SQL语句在MySQL中的执行过程</h5><h6 id="MySQL-基础架构分析"><a href="#MySQL-基础架构分析" class="headerlink" title="MySQL 基础架构分析"></a>MySQL 基础架构分析</h6><p> MySQL 主要分为 Server 层和存储引擎层：</p>
<ul>
<li><strong>Server 层</strong>：主要包括连接器、查询缓存、分析器、优化器、执行器等，所有跨存储引擎的功能都在这一层实现，比如存储过程、触发器、视图，函数等，还有一个通用的日志模块 binlog 日志模块。</li>
<li><strong>存储引擎</strong>： 主要负责数据的存储和读取，采用可以替换的插件式架构，支持 InnoDB、MyISAM、Memory 等多个存储引擎，其中 InnoDB 引擎有自有的日志模块 redolog 模块。<strong>现在最常用的存储引擎是 InnoDB，它从 MySQL 5.5 版本开始就被当做默认存储引擎了。</strong></li>
</ul>
<hr>
<h6 id="语句分析"><a href="#语句分析" class="headerlink" title="语句分析"></a>语句分析</h6><p><strong>更新语句</strong></p>
<p>以 InnoDB 模式下来探讨这个语句的执行流程。流程如下：</p>
<ul>
<li>先查询到张三这一条数据，如果有缓存，也是会用到缓存。</li>
<li>然后拿到查询的语句，把 age 改为 19，然后调用引擎 API 接口，写入这一行数据，InnoDB 引擎把数据保存在内存中，同时记录 redo log，此时 redo log 进入 prepare 状态，然后告诉执行器，执行完成了，随时可以提交。</li>
<li>执行器收到通知后记录 binlog，然后调用引擎接口，提交 redo log 为提交状态。</li>
<li>更新完成。</li>
</ul>
<hr>
<h6 id="总结-7"><a href="#总结-7" class="headerlink" title="总结"></a>总结</h6><ul>
<li>MySQL 主要分为 Server 层和引擎层，Server 层主要包括连接器、查询缓存、分析器、优化器、执行器，同时还有一个日志模块（binlog），这个日志模块所有执行引擎都可以共用，redolog 只有 InnoDB 有。</li>
<li>引擎层是插件式的，目前主要包括，MyISAM,InnoDB,Memory 等。</li>
<li>查询语句的执行流程如下：权限校验（如果命中缓存）—&gt;查询缓存—&gt;分析器—&gt;优化器—&gt;权限校验—&gt;执行器—&gt;引擎</li>
<li>更新语句执行流程如下：分析器—-&gt;权限校验—-&gt;执行器—&gt;引擎—redo log(prepare 状态)—&gt;binlog—&gt;redo log(commit状态)</li>
</ul>
<hr>
<h5 id="MySQL执行计划分析"><a href="#MySQL执行计划分析" class="headerlink" title="MySQL执行计划分析"></a>MySQL执行计划分析</h5><hr>
<h6 id="什么是执行计划？"><a href="#什么是执行计划？" class="headerlink" title="什么是执行计划？"></a>什么是执行计划？</h6><p><strong>执行计划</strong> 是指一条 SQL 语句在经过 <strong>MySQL 查询优化器</strong> 的优化会后，具体的执行方式。</p>
<hr>
<h6 id="如何获取执行计划？"><a href="#如何获取执行计划？" class="headerlink" title="如何获取执行计划？"></a>如何获取执行计划？</h6><hr>
<h6 id="如何分析-EXPLAIN-结果？"><a href="#如何分析-EXPLAIN-结果？" class="headerlink" title="如何分析 EXPLAIN 结果？"></a>如何分析 EXPLAIN 结果？</h6><p>&#x3D;&#x3D;<strong>type（重要）</strong>&#x3D;&#x3D;</p>
<p>查询执行的类型，描述了查询是如何执行的。所有值的顺序从最优到最差排序为：system &gt; const &gt; eq_ref &gt; ref &gt; fulltext &gt; ref_or_null &gt; index_merge &gt; unique_subquery &gt; index_subquery &gt; range &gt; index &gt; ALL</p>
<p>常见的几种类型具体含义如下：</p>
<ul>
<li><strong>system</strong>：如果表使用的引擎对于表行数统计是精确的（如：MyISAM），且表中只有一行记录的情况下，访问方法是 system ，是 const 的一种特例。</li>
<li><strong>const</strong>：表中最多只有一行匹配的记录，一次查询就可以找到，常用于使用主键或唯一索引的所有字段作为查询条件。</li>
<li><strong>eq_ref</strong>：当连表查询时，前一张表的行在当前这张表中只有一行与之对应。是除了 system 与 const 之外最好的 join 方式，常用于使用主键或唯一索引的所有字段作为连表条件。</li>
<li><strong>ref</strong>：使用普通索引作为查询条件，查询结果可能找到多个符合条件的行。</li>
<li><strong>index_merge</strong>：当查询条件使用了多个索引时，表示开启了 Index Merge 优化，此时执行计划中的 key 列列出了使用到的索引。</li>
<li><strong>range</strong>：对索引列进行范围查询，执行计划中的 key 列表示哪个索引被使用了。</li>
<li><strong>index</strong>：查询遍历了整棵索引树，与 ALL 类似，只不过扫描的是索引，而索引一般在内存中，速度更快。</li>
<li><strong>ALL</strong>：全表扫描。</li>
</ul>
<hr>
<p><strong>&#x3D;&#x3D;key（重要）&#x3D;&#x3D;</strong></p>
<p>key 列表示 MySQL 实际使用到的索引。如果为 NULL，则表示未用到索引。</p>
<hr>
<p><strong>&#x3D;&#x3D;Extra（重要）&#x3D;&#x3D;</strong></p>
<p>这列包含了 MySQL 解析查询的额外信息，通过这些信息，可以更准确的理解 MySQL 到底是如何执行查询的。常见的值如下：</p>
<ul>
<li><strong>Using filesort</strong>：在排序时使用了外部的索引排序，没有用到表内索引进行排序。</li>
<li><strong>Using temporary</strong>：MySQL 需要创建临时表来存储查询的结果，常见于 ORDER BY 和 GROUP BY。</li>
<li><strong>Using index</strong>：表明查询使用了覆盖索引，不用回表，查询效率非常高。</li>
<li><strong>Using index condition</strong>：表示查询优化器选择使用了索引条件下推这个特性。</li>
<li><strong>Using where</strong>：表明查询使用了 WHERE 子句进行条件过滤。一般在没有使用到索引的时候会出现。</li>
<li>**Using join buffer (Block Nested Loop)**：连表查询的方式，表示当被驱动表的没有使用索引的时候，MySQL 会先将驱动表读出来放到 join buffer 中，再遍历被驱动表与驱动表进行查询。</li>
</ul>
<hr>
<h5 id="MySQL自增主键一定是连续的吗"><a href="#MySQL自增主键一定是连续的吗" class="headerlink" title="MySQL自增主键一定是连续的吗"></a>MySQL自增主键一定是连续的吗</h5><hr>
<h6 id="自增值保存在哪里"><a href="#自增值保存在哪里" class="headerlink" title="自增值保存在哪里"></a>自增值保存在哪里</h6><p><strong>到了 MySQL 8.0 版本后，自增值的变更记录被放在了 redo log 中，提供了自增值持久化的能力</strong> ，也就是实现了“如果发生重启，表的自增值可以根据 redo log 恢复为 MySQL 重启前的值”</p>
<hr>
<h6 id="自增值不连续的场景"><a href="#自增值不连续的场景" class="headerlink" title="自增值不连续的场景"></a>自增值不连续的场景</h6><p>自增值不连续场景 1：</p>
<p>在 MySQL 里面，如果字段 id 被定义为 AUTO_INCREMENT，在插入一行数据的时候，自增值的行为如下：</p>
<ul>
<li>如果插入数据时 id 字段指定为 0、null 或未指定值，那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段；</li>
<li>如果插入数据时 id 字段指定了具体的值，就直接使用语句里指定的值。</li>
</ul>
<p>根据要插入的值和当前自增值的大小关系，自增值的变更结果也会有所不同。假设某次要插入的值是 <code>insert_num</code>，当前的自增值是 <code>autoIncrement_num</code>：</p>
<ul>
<li>如果 <code>insert_num &lt; autoIncrement_num</code>，那么这个表的自增值不变</li>
<li>如果 <code>insert_num &gt;= autoIncrement_num</code>，就需要把当前自增值修改为新的自增值</li>
</ul>
<p>也就是说，如果插入的 id 是 100，当前的自增值是 90，<code>insert_num &gt;= autoIncrement_num</code>，那么从 <code>auto_increment_offset</code> 开始，以 <code>auto_increment_increment</code> 为步长，持续叠加，直到找到第一个大于 100 的值，作为新的自增值。</p>
<hr>
<h6 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h6><p>本文总结下自增值不连续的 4 个场景：</p>
<ol>
<li>自增初始值和自增步长设置不为 1</li>
<li>唯一键冲突</li>
<li>事务回滚</li>
<li>批量插入（如 <code>insert...select</code> 语句）</li>
</ol>
<hr>
<h5 id="MySQL时间类型数据存储建议"><a href="#MySQL时间类型数据存储建议" class="headerlink" title="MySQL时间类型数据存储建议"></a>MySQL时间类型数据存储建议</h5><hr>
<h6 id="不要用字符串存储日期"><a href="#不要用字符串存储日期" class="headerlink" title="不要用字符串存储日期"></a>不要用字符串存储日期</h6><hr>
<h6 id="Datetime-和-Timestamp-之间抉择"><a href="#Datetime-和-Timestamp-之间抉择" class="headerlink" title="Datetime 和 Timestamp 之间抉择"></a>Datetime 和 Timestamp 之间抉择</h6><p><strong>通常我们都会首选 Timestamp。</strong> 下面说一下为什么这样做!</p>
<p><strong>DateTime 类型没有时区信息</strong></p>
<p><strong>DateTime 类型是没有时区信息的（时区无关）</strong> ，DateTime 类型保存的时间都是当前会话所设置的时区对应的时间。当你的时区更换之后，比如你的服务器更换地址或者更换客户端连接时区设置的话，就会导致你从数据库中读出的时间错误。</p>
<p><strong>Timestamp 和时区有关</strong>。Timestamp 类型字段的值会随着服务器时区的变化而变化，自动换算成相应的时间，说简单点就是在不同时区，查询到同一个条记录此字段的值会不一样。</p>
<p><strong>扩展：一些关于 MySQL 时区设置的一个常用 sql 命令</strong></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"># 查看当前会话时区</span><br><span class="line">SELECT @@session.time_zone;</span><br><span class="line"># 设置当前会话时区</span><br><span class="line">SET time_zone = &#x27;Europe/Helsinki&#x27;;</span><br><span class="line">SET time_zone = &quot;+00:00&quot;;</span><br><span class="line"># 数据库全局时区设置</span><br><span class="line">SELECT @@global.time_zone;</span><br><span class="line"># 设置全局时区</span><br><span class="line">SET GLOBAL time_zone = &#x27;+8:00&#x27;;</span><br><span class="line">SET GLOBAL time_zone = &#x27;Europe/Helsinki&#x27;;</span><br></pre></td></tr></table></figure>

<hr>
<h6 id="数值型-x3D-x3D-时间戳-x3D-x3D-是更好的选择吗？"><a href="#数值型-x3D-x3D-时间戳-x3D-x3D-是更好的选择吗？" class="headerlink" title="数值型&#x3D;&#x3D;时间戳&#x3D;&#x3D;是更好的选择吗？"></a>数值型&#x3D;&#x3D;时间戳&#x3D;&#x3D;是更好的选择吗？</h6><p>这种存储方式的具有 Timestamp 类型的所具有一些优点，并且使用它的进行日期排序以及对比等操作的效率会更高，跨系统也很方便，毕竟只是存放的数值。缺点也很明显，就是数据的可读性太差了，你无法直观的看到具体时间。</p>
<hr>
<h6 id="总结-8"><a href="#总结-8" class="headerlink" title="总结"></a>总结</h6><p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/%E6%80%BB%E7%BB%93-%E5%B8%B8%E7%94%A8%E6%97%A5%E6%9C%9F%E5%AD%98%E5%82%A8%E6%96%B9%E5%BC%8F.jpg" alt="img"></p>
<hr>
<h5 id="MySQL隐式转换造成索引失效"><a href="#MySQL隐式转换造成索引失效" class="headerlink" title="MySQL隐式转换造成索引失效"></a>MySQL隐式转换造成索引失效</h5><hr>
<h3 id="Redis"><a href="#Redis" class="headerlink" title="Redis"></a>Redis</h3><h4 id="Redis常见面试题总结-上"><a href="#Redis常见面试题总结-上" class="headerlink" title="Redis常见面试题总结(上)"></a>Redis常见面试题总结(上)</h4><h5 id="Redis-基础"><a href="#Redis-基础" class="headerlink" title="Redis 基础"></a>Redis 基础</h5><h6 id="Redis-为什么这么快？"><a href="#Redis-为什么这么快？" class="headerlink" title="Redis 为什么这么快？"></a>Redis 为什么这么快？</h6><p>Redis 内部做了非常多的性能优化，比较重要的主要有下面 3 点：</p>
<ul>
<li>Redis 基于内存，内存的访问速度是磁盘的上千倍；</li>
<li>Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型，主要是单线程事件循环和 IO 多路复用（Redis 线程模式后面会详细介绍到）；</li>
<li>Redis 内置了多种优化过后的数据结构实现，性能非常高。</li>
</ul>
<hr>
<h6 id="Redis-数据结构"><a href="#Redis-数据结构" class="headerlink" title="Redis 数据结构"></a>Redis 数据结构</h6><p><strong>Redis 常用的数据结构有哪些？</strong></p>
<ul>
<li><strong>5 种基础数据结构</strong> ：String（字符串）、List（列表）、Set（集合）、Hash（散列）、Zset（有序集合）。</li>
<li><strong>3 种特殊数据结构</strong> ：HyperLogLogs（基数统计）、Bitmap （位存储）、Geospatial (地理位置)。</li>
</ul>
<p>&#x3D;&#x3D;Redis 5 种基本数据结构详解&#x3D;&#x3D;</p>
<p>Redis 共有 5 种基本数据结构：String（字符串）、List（列表）、Set（集合）、Hash（散列）、Zset（有序集合）。</p>
<p>这 5 种数据结构是直接提供给用户使用的，是数据的保存形式，其底层实现主要依赖这 8 种数据结构：简单动态字符串（SDS）、LinkedList（双向链表）、Hash Table（哈希表）、SkipList（跳跃表）、Intset（整数集合）、ZipList（压缩列表）、QuickList（快速列表）。</p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230308141312344.png" alt="image-20230308141312344"></p>
<p>&#x3D;&#x3D;Redis 3 种特殊数据结构详解&#x3D;&#x3D;</p>
<hr>
<h6 id="Redis-线程模型"><a href="#Redis-线程模型" class="headerlink" title="Redis 线程模型"></a>Redis 线程模型</h6><p> <strong>Redis 单线程模型</strong>：<strong>Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型</strong>，</p>
<p><strong>虽然文件事件处理器以单线程方式运行，但通过使用 I&#x2F;O 多路复用程序来监听多个套接字</strong></p>
<p><strong>既然是单线程，那怎么监听大量的客户端连接呢？</strong></p>
<p>Redis 通过 <strong>IO 多路复用程序</strong> 来监听来自客户端的大量连接（或者说是监听多个 socket），它会将感兴趣的事件及类型（读、写）注册到内核中并监听每个事件是否发生。</p>
<p>这样的好处非常明显： <strong>I&#x2F;O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接，降低了资源的消耗</strong>（和 NIO 中的 <code>Selector</code> 组件很像）。</p>
<hr>
<h6 id="Redis-内存管理"><a href="#Redis-内存管理" class="headerlink" title="Redis 内存管理"></a>Redis 内存管理</h6><p><strong>过期的数据的删除策略了解么</strong>？</p>
<p>常用的过期数据的删除策略就两个（重要！自己造缓存轮子的时候需要格外考虑的东西）：</p>
<ol>
<li><strong>惰性删除</strong> ：只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好，但是可能会造成太多过期 key 没有被删除。</li>
<li><strong>定期删除</strong> ： 每隔一段时间抽取一批 key 执行删除过期 key 操作。并且，Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。</li>
</ol>
<p> Redis 采用的是 <strong>定期删除+惰性&#x2F;懒汉式删除</strong> 。</p>
<p>但是，仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里，然后就 Out of memory 了。</p>
<p>怎么解决这个问题呢？答案就是：<strong>Redis 内存淘汰机制。</strong></p>
<hr>
<p> <strong>Redis 内存淘汰机制了解么？</strong></p>
<hr>
<p><strong>&#x3D;&#x3D;Redis 持久化机制&#x3D;&#x3D;</strong></p>
<p> <strong>Redis 的一种持久化方式叫快照（snapshotting，RDB），另一种方式是只追加文件（append-only file, AOF）</strong>。</p>
<ol>
<li>RDB 持久化（默认方式）：Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。将其备份或者复制到其他服务器。save:主线程执行，会阻塞主线程；bgsave:子线程执行，不会阻塞主线程，默认选项。</li>
<li>AOF 持久化:开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令，Redis 就会将该命令写入到内存缓存 <code>server.aof_buf</code> 中，然后再根据 <code>appendfsync</code> 配置来决定何时将其同步到硬盘中的 AOF 文件。</li>
</ol>
<p><strong>AOF 日志是如何实现的？</strong></p>
<p>关系型数据库（如 MySQL）通常都是执行命令之前记录日志（方便故障恢复），而 Redis AOF 持久化机制是在执行完命令之后再记录日志。</p>
<p><strong>为什么是在执行完命令之后记录日志呢？</strong></p>
<ul>
<li>避免额外的检查开销，AOF 记录日志不会对命令进行语法检查；</li>
<li>在命令执行完之后再记录，不会阻塞当前的命令执行。</li>
</ul>
<p>这样也带来了风险（我在前面介绍 AOF 持久化的时候也提到过）：</p>
<ul>
<li>如果刚执行完命令 Redis 就宕机会导致对应的修改丢失；</li>
<li>可能会阻塞后续其他命令的执行（AOF 记录日志是在 Redis 主线程中进行的）。</li>
</ul>
<hr>
<p><strong>如何选择 RDB 和 AOF？</strong></p>
<p><strong>RDB 比 AOF 优秀的地方</strong> ：</p>
<ul>
<li>RDB 文件存储的内容是经过压缩的二进制数据， 保存着某个时间点的数据集，文件很小，适合做数据的备份，灾难恢复。AOF 文件存储的是每一次写命令，类似于 MySQL 的 binlog 日志，通常会必 RDB 文件大很多。当 AOF 变得太大时，Redis 能够在后台自动重写 AOF。新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样，但体积更小。不过， Redis 7.0 版本之前，如果在重写期间有写入命令，AOF 可能会使用大量内存，重写期间到达的所有写入命令都会写入磁盘两次。</li>
<li>使用 RDB 文件恢复数据，直接解析还原数据即可，不需要一条一条地执行命令，速度非常快。而 AOF 则需要依次执行每个写命令，速度非常慢。也就是说，与 AOF 相比，恢复大数据集的时候，RDB 速度更快。</li>
</ul>
<p><strong>AOF 比 RDB 优秀的地方</strong> ：</p>
<ul>
<li>RDB 的数据安全性不如 AOF，没有办法实时或者秒级持久化数据。生成 RDB 文件的过程是比繁重的， 虽然 BGSAVE 子进程写入 RDB 文件的工作不会阻塞主线程，但会对机器的 CPU 资源和内存资源产生影响，严重的情况下甚至会直接把 Redis 服务干宕机。AOF 支持秒级数据丢失（取决 fsync 策略，如果是 everysec，最多丢失 1 秒的数据），仅仅是追加命令到 AOF 文件，操作轻量。</li>
<li>RDB 文件是以特定的二进制格式保存的，并且在 Redis 版本演进中有多个版本的 RDB，所以存在老版本的 Redis 服务不兼容新版本的 RDB 格式的问题。</li>
<li>AOF 以一种易于理解和解析的格式包含所有操作的日志。你可以轻松地导出 AOF 文件进行分析，你也可以直接操作 AOF 文件来解决一些问题。比如，如果执行<code>FLUSHALL</code>命令意外地刷新了所有内容后，只要 AOF 文件没有被重写，删除最新命令并重启即可恢复之前的状态。</li>
</ul>
<hr>
<p>Redis 4.0 对于持久化机制做了什么优化？</p>
<p>由于 RDB 和 AOF 各有优势，于是，Redis 4.0 开始支持 RDB 和 AOF 的&#x3D;&#x3D;混合持久化&#x3D;&#x3D;（默认关闭，可以通过配置项 <code>aof-use-rdb-preamble</code> 开启）。</p>
<p>如果把混合持久化打开，AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的， AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式，可读性较差。</p>
<hr>
<h4 id="Redis常见面试题总结-下"><a href="#Redis常见面试题总结-下" class="headerlink" title="Redis常见面试题总结(下)"></a>Redis常见面试题总结(下)</h4><h5 id="Redis-事务"><a href="#Redis-事务" class="headerlink" title="Redis 事务"></a>Redis 事务</h5><p>如何解决 Redis 事务的缺陷？</p>
<p>Redis 从 2.6 版本开始支持执行 Lua 脚本，它的功能和事务非常类似。我们可以利用 Lua 脚本来批量执行多条 Redis 命令，这些 Redis 命令会被提交到 Redis 服务器一次性执行完成，大幅减小了网络开销。</p>
<p>一段 Lua 脚本可以视作一条命令执行，一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行，保证了操作不会被其他指令插入或打扰。</p>
<hr>
<h6 id="Redis-性能优化"><a href="#Redis-性能优化" class="headerlink" title="Redis 性能优化"></a>Redis 性能优化</h6><p>大量 key 集中过期问题</p>
<p>我在上面提到过：对于过期 key，Redis 采用的是 <strong>定期删除+惰性&#x2F;懒汉式删除</strong> 策略。</p>
<p>定期删除执行过程中，如果突然遇到大量过期 key 的话，客户端请求必须等待定期清理过期 key 任务线程执行完成，因为这个这个定期任务线程是在 Redis 主线程中执行的。这就导致客户端请求没办法被及时处理，响应速度会比较慢。</p>
<p>如何解决呢？下面是两种常见的方法：</p>
<ol>
<li>给 key 设置随机过期时间。</li>
<li>开启 lazy-free（惰性删除&#x2F;延迟释放） 。lazy-free 特性是 Redis 4.0 开始引入的，指的是让 Redis 采用异步方式延迟释放 key 使用的内存，将该操作交给单独的子线程处理，避免阻塞主线程。</li>
</ol>
<p>个人建议不管是否开启 lazy-free，我们都尽量给 key 设置随机过期时间。</p>
<hr>
<h6 id="Redis-生产问题"><a href="#Redis-生产问题" class="headerlink" title="Redis 生产问题"></a>Redis 生产问题</h6><p>&#x3D;&#x3D;缓存穿透&#x3D;&#x3D;</p>
<p>缓存穿透说简单点就是大量请求的 key 是不合理的，<strong>根本不存在于缓存中，也不存在于数据库中</strong> 。这就导致这些请求直接到了数据库上，根本没有经过缓存这一层，对数据库造成了巨大的压力，可能直接就被这么多请求弄宕机了。</p>
<p>解决方法：</p>
<ol>
<li><strong>缓存无效key</strong></li>
<li><strong>布隆过滤器</strong>：<strong>布隆过滤器说某个元素存在，小概率会误判。布隆过滤器说某个元素不在，那么这个元素一定不在。</strong></li>
</ol>
<p>&#x3D;&#x3D;缓存击穿&#x3D;&#x3D;</p>
<p>缓存击穿中，请求的 key 对应的是 <strong>热点数据</strong> ，该数据 <strong>存在于数据库中，但不存在于缓存中（通常是因为缓存中的那份数据已经过期）</strong> 。这就可能会导致瞬时大量的请求直接打到了数据库上，对数据库造成了巨大的压力，可能直接就被这么多请求弄宕机了。</p>
<p>有哪些解决办法？</p>
<ul>
<li>设置热点数据永不过期或者过期时间比较长。</li>
<li>针对热点数据提前预热，将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。</li>
<li>请求数据库写数据到缓存之前，先获取互斥锁，保证只有一个请求会落到数据库上，减少数据库的压力。</li>
</ul>
<p><strong>缓存穿透和缓存击穿有什么区别？</strong></p>
<p>缓存穿透中，请求的 key 既不存在于缓存中，也不存在于数据库中。</p>
<p>缓存击穿中，请求的 key 对应的是 <strong>热点数据</strong> ，该数据 <strong>存在于数据库中，但不存在于缓存中（通常是因为缓存中的那份数据已经过期）</strong></p>
<p>&#x3D;&#x3D;缓存雪崩&#x3D;&#x3D; </p>
<p><strong>缓存在同一时间大面积的失效，导致大量的请求都直接落到了数据库上，对数据库造成了巨大的压力。</strong></p>
<p><strong>针对 Redis 服务不可用的情况：</strong></p>
<ol>
<li>采用 Redis 集群，避免单机出现问题整个缓存服务都没办法使用。</li>
<li>限流，避免同时处理大量的请求。</li>
</ol>
<p><strong>针对热点缓存失效的情况：</strong></p>
<ol>
<li>设置不同的失效时间比如随机设置缓存的失效时间。</li>
<li>缓存永不失效（不太推荐，实用性太差）。</li>
<li>设置二级缓存。</li>
</ol>
<hr>
<h4 id="x3D-x3D-哨兵（自动故障转移）-x3D-x3D"><a href="#x3D-x3D-哨兵（自动故障转移）-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;哨兵（自动故障转移）&#x3D;&#x3D;"></a>&#x3D;&#x3D;哨兵（自动故障转移）&#x3D;&#x3D;</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">sentinel monitor resque 192.168.1.3 6380 4</span><br><span class="line">//如果服务器在给定的毫秒数之内， 没有返回 Sentinel 发送的 PING 命令的回复， 或者返回一个错误， 那么 Sentinel 将这个服务器标记为主观下线（subjectively down，简称 SDOWN ）。</span><br><span class="line">不过只有一个 Sentinel 将服务器标记为主观下线并不一定会引起服务器的自动故障迁移： 只有在足够数量的 Sentinel 都将一个服务器标记为主观下线之后， 服务器才会被标记为客观下线（objectively down， 简称 ODOWN ）， 这时自动故障迁移才会执行。</span><br><span class="line">sentinel down-after-milliseconds resque 10000</span><br><span class="line">//</span><br><span class="line">sentinel failover-timeout resque 180000</span><br><span class="line">// 在发⽣主备切换时最多可以有 5 个 slave 同时对新的 master 进⾏同步,数字太大导致集群不可用，太小导致转移时间长</span><br><span class="line">sentinel parallel-syncs resque 5</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><img src="E:\XxdBlog\source_posts\images\test\image-20230321140816128.png" alt="image-20230321140816128"></p>
<p>在复制的基础上，哨兵实现了自动化故障恢复的功能，哨兵功能描述：</p>
<ul>
<li>监控：监控所有 redis 节点（包括 sentinel 节点⾃身）的状态是否正常。 </li>
<li>故障转移：如果⼀个 master 出现故障，Sentinel 会帮助我们实现故障转移， ⾃动将某⼀台 slave 升级为 master，确保整个 Redis 系统的可⽤性。 </li>
<li>通知 ：通知 slave 新的 master 连接信息，让它们执⾏ replicaof 成为新的 master 的 slave。 </li>
<li>配置提供 ：客户端连接 sentinel 请求 master 的地址，如果发⽣故障转移， sentinel 会通知新的 master 链接信息给客户端。</li>
</ul>
<p><strong>如果想要实现⾼可⽤，建议将哨兵 Sentinel 配置成单数且⼤于等于 3 台。（&#x3D;&#x3D;哨兵集群&#x3D;&#x3D;）</strong></p>
<ul>
<li>多Sentinel节点通过投票确定节点是否真的不可用，避免误判（比如网络问题导致的误判）</li>
<li>Sentinel自身就是高可用的</li>
</ul>
<h5 id="Sentinel-如何检测节点是否下线？"><a href="#Sentinel-如何检测节点是否下线？" class="headerlink" title="Sentinel 如何检测节点是否下线？"></a>Sentinel 如何检测节点是否下线？</h5><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">相关的问题：</span><br><span class="line">● 主观下线与客观下线的区别?</span><br><span class="line">● Sentinel 是如何实现故障转移的？</span><br><span class="line">● 为什么建议部署多个 sentinel 节点（哨兵集群）？</span><br></pre></td></tr></table></figure>

<ul>
<li><strong>主观下线(SDOWN)</strong> ：sentinel 节点认为某个 Redis 节点已经下线了（主观下 线），但还不是很确定，需要其他 sentinel 节点的投票。 <ul>
<li>sentinel 节点以某一频率向其他节点发送ping命令，超过规定时间（down-after-millisenconds）没有回复pong、-loading、-masterdown消息，认为是主观下线</li>
</ul>
</li>
<li><strong>客观下线(ODOWN)</strong> ：法定数量（通常为过半）的 sentinel 节点认定某个 Redis 节点已经下线（客观下线），那它就算是真的下线了。</li>
</ul>
<h5 id="Sentinel-如何选择出新的-master？（优先级从高到低）"><a href="#Sentinel-如何选择出新的-master？（优先级从高到低）" class="headerlink" title="Sentinel 如何选择出新的 master？（优先级从高到低）"></a>Sentinel 如何选择出新的 master？（优先级从高到低）</h5><ol>
<li><strong>slave 优先级</strong> ：可以通过 slave-priority ⼿动设置 slave 的优先级，优先级越 ⾼得分越⾼，优先级最⾼的直接成为新的 master。如果没有优先级最⾼的， 再判断复制进度。 </li>
<li><strong>复制进度</strong> ：Sentinel 总是希望选择出数据最完整（与旧 master 数据最接 近）也就是复制进度最快的 slave 被提升为新的 master，复制进度越快得分 也就越⾼。</li>
<li><strong>runid(运⾏ id)</strong> ：通常经过前⾯两轮筛选已经成果选出来了新的 master，万 ⼀真有多个 slave 的优先级和复制进度⼀样的话，那就 runid ⼩的成为新的 master，每个 redis 节点启动时都有⼀个 40 字节随机字符串作为运⾏ id。</li>
</ol>
<h5 id="如何从-Sentinel-集群中选择出-Leader-（从sentinel集群选择一个leader复制完成故障转移）？"><a href="#如何从-Sentinel-集群中选择出-Leader-（从sentinel集群选择一个leader复制完成故障转移）？" class="headerlink" title="如何从 Sentinel 集群中选择出 Leader （从sentinel集群选择一个leader复制完成故障转移）？"></a>如何从 Sentinel 集群中选择出 Leader （从sentinel集群选择一个leader复制完成故障转移）？</h5><h6 id="如何选择出-Leader-⻆⾊呢？"><a href="#如何选择出-Leader-⻆⾊呢？" class="headerlink" title="如何选择出 Leader ⻆⾊呢？"></a>如何选择出 Leader ⻆⾊呢？</h6><p>分布式领域的<font color='orange'>共识算法</font>（大部分基于 Paxos 算法改进⽽来），在 sentinel 选举 leader 这个场景 下使⽤的是 <font color='cornflowerblue'>Raft </font>算法。</p>
<hr>
<h5 id="Sentinel-可以防⽌脑裂吗？"><a href="#Sentinel-可以防⽌脑裂吗？" class="headerlink" title="Sentinel 可以防⽌脑裂吗？"></a>Sentinel 可以防⽌脑裂吗？</h5><p><img src="https://img-blog.csdnimg.cn/20200108155524948.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,ow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTU3OTc4MA==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述"></p>
<p>因为网络问题，导致sentinel（哨兵）与master（主节点）通讯中断，当哨兵找不到master的时候，认为master宕机；需要选举一个slave(备份节点）作为主节点。旧master上的客户端连接如果与master未中断，可以继续写入数据；新的master处理新客户端的写操作。这时候两个master数据不一致。当旧的master与哨兵恢复通讯时,旧的master就会降级为slave。新的master将新写入的数据，同步到旧的master中，但是缺少旧的master中写入的数据,所以数据丢失。</p>
<p>可以，对主从复制进行配置即可。</p>
<ul>
<li>min-replicas-to-write 1：⽤于配置写 master 时⾄少写⼊的 slave 数量，设 置为 0 表示关闭该功能。3表示master至少写入3个slave才可以接受写入命令。</li>
<li>min-replicas-max-lag 10 ： master 多⻓时间（秒）⽆法得到从 节点的响应，就认为这个从节点失联。</li>
</ul>
<h4 id="x3D-x3D-集群（Cluster）-x3D-x3D"><a href="#x3D-x3D-集群（Cluster）-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;集群（Cluster）&#x3D;&#x3D;"></a>&#x3D;&#x3D;集群（Cluster）&#x3D;&#x3D;</h4><h5 id="为什么需要-Redis-Cluster？"><a href="#为什么需要-Redis-Cluster？" class="headerlink" title="为什么需要 Redis Cluster？"></a>为什么需要 Redis Cluster？</h5><ol>
<li>缓存的数据量太大</li>
<li>并发量要求太大</li>
</ol>
<p><strong>Redis 切⽚集群对于<font color='cornflowerblue'>横向扩展</font>⾮常友好，只需要增加 Redis 节点到集群中即可</strong></p>
<hr>
<h5 id="⼀个最基本的-Redis-Cluster-架构是怎样的？"><a href="#⼀个最基本的-Redis-Cluster-架构是怎样的？" class="headerlink" title="⼀个最基本的 Redis Cluster 架构是怎样的？"></a>⼀个最基本的 Redis Cluster 架构是怎样的？</h5><p> slave 不对外提供读服务，主要⽤来保障 master 的⾼可⽤，当 master 出现故障的时候替代它。</p>
<p>Redis Cluster 是去中⼼化的（各个节点基于 Gossip 进⾏通信），任何⼀个 master 出现故障，其它的 master 节点不受影响。</p>
<p>如果宕机的 master ⽆ slave 的话，为了保障集群的完整性，保证所有的&#x3D;&#x3D;哈希槽&#x3D;&#x3D;都 指派给了可⽤的 master ，整个集群将不可⽤。（可设置参数解决不可用）</p>
<h6 id="哈希槽"><a href="#哈希槽" class="headerlink" title="哈希槽"></a>哈希槽</h6><p><strong>一个 redis 集群包含 16384 个哈希槽（hash slot）</strong>，数据库中的每个数据都属于这16384个哈希槽中的一个。集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽。集群中的每一个节点负责处理一部分哈希槽。</p>
<h6 id="增加删除节点"><a href="#增加删除节点" class="headerlink" title="增加删除节点"></a>增加删除节点</h6><p><a target="_blank" rel="noopener" href="https://blog.csdn.net/wb1046329430/article/details/119347684?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168068690216800217273923%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=168068690216800217273923&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~baidu_landing_v2~default-1-119347684-null-null.142%5Ev81%5Einsert_down1,201%5Ev4%5Eadd_ask,239%5Ev2%5Einsert_chatgpt&utm_term=cluster%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9&spm=1018.2226.3001.4187">https://blog.csdn.net/wb1046329430/article/details/119347684?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168068690216800217273923%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&amp;request_id=168068690216800217273923&amp;biz_id=0&amp;utm_medium=distribute.pc_search_result.none-task-blog-2~all~baidu_landing_v2~default-1-119347684-null-null.142^v81^insert_down1,201^v4^add_ask,239^v2^insert_chatgpt&amp;utm_term=cluster%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9&amp;spm=1018.2226.3001.4187</a></p>
<h5 id="Redis-Cluster-是如何分⽚的？"><a href="#Redis-Cluster-是如何分⽚的？" class="headerlink" title="Redis Cluster 是如何分⽚的？"></a>Redis Cluster 是如何分⽚的？</h5><p><img src="https://img2020.cnblogs.com/blog/1552449/202109/1552449-20210930011640764-603796536.png" alt="img"></p>
<p>Redis Cluster 并&#x3D;&#x3D;没有使⽤⼀致性哈希&#x3D;&#x3D;，采⽤的是 <strong>&#x3D;&#x3D;哈希槽分区&#x3D;&#x3D;</strong> ，每⼀个键值对都 属于⼀个 hash slot（哈希槽） ，Redis Cluster 通常有 16384 个哈希槽。</p>
<p>分区机制优点：<strong>解耦了数据和节点之间的关系，提升了集群的横向扩展性和容错性。</strong></p>
<p><strong>和一致性哈希相比</strong></p>
<ol>
<li>它并不是闭合的，key的定位规则是<strong>根据CRC-16(key)%16384的值来判断属于哪个槽区，从而判断该key属于哪个节点</strong>，而一致性哈希是根据hash(key)的值来顺时针找第一个hash(ip)的节点，从而确定key存储在哪个节点。</li>
<li>一致性哈希是创建虚拟节点来实现节点宕机后的数据转移并保证数据的安全性和集群的可用性的。redis cluster是采用master节点有多个slave节点机制来保证数据的完整性的,master节点写入数据，slave节点同步数据。当master节点挂机后，slave节点会通过选举机制选举出一个节点变成master节点，实现高可用。但是这里有一点需要考虑，如果master节点存在热点缓存，某一个时刻某个key的访问急剧增高，这时该mater节点可能操劳过度而死，随后从节点选举为主节点后，同样宕机，一次类推，造成缓存雪崩.</li>
</ol>
<h5 id="为什么-Redis-Cluster-的哈希槽是-16384（2的14次方）-个"><a href="#为什么-Redis-Cluster-的哈希槽是-16384（2的14次方）-个" class="headerlink" title="为什么 Redis Cluster 的哈希槽是 16384（2的14次方） 个?"></a>为什么 Redis Cluster 的哈希槽是 16384（2的14次方） 个?</h5><p>正常的⼼跳包会携带⼀个节点的完整配置，它会以幂等的⽅式更新旧的配置，这意味着⼼跳包会附带当前节点的负责的哈希槽的信息。假设哈希槽采⽤16384 ,则占空间 2k(16384&#x2F;8)。假设哈希槽采⽤ 65536， 则占空间 8k(65536&#x2F;8)，这是令⼈难以接受的内存占⽤。 由于其他设计上的权衡，Redis Cluster 不太可能扩展到超过 1000 个主节点。</p>
<p>消息传输过程中，会对 myslots 进⾏压缩，bitmap 的填充率越低，压缩率越⾼。 bitmap 的填充率的值是 <strong>哈希槽总数&#x2F;节点数</strong> ，如果哈希槽总数太⼤的话， bitmap 的填充率的值也会⽐较⼤。</p>
<p><font color='orange'>最后，总结⼀下 Redis Cluster 的哈希槽的数量选择 16384 ⽽不是 65536 的主要 原因： </font></p>
<ul>
<li>哈希槽太⼤会导致⼼跳包太⼤，消耗太多带宽； </li>
<li>哈希槽总数越少，对存储哈希槽信息的 bitmap 压缩效果越好； </li>
<li>Redis Cluster 的主节点通常不会扩展太多，16384 个哈希槽已经⾜够⽤了。</li>
</ul>
<hr>
<h5 id="Redis-Cluster-如何重新分配哈希槽？"><a href="#Redis-Cluster-如何重新分配哈希槽？" class="headerlink" title="Redis Cluster 如何重新分配哈希槽？"></a>Redis Cluster 如何重新分配哈希槽？</h5><p>Cluster内置相关命令</p>
<hr>
<h5 id="Redis-Cluster-扩容缩容期间可以提供服务吗？"><a href="#Redis-Cluster-扩容缩容期间可以提供服务吗？" class="headerlink" title="Redis Cluster 扩容缩容期间可以提供服务吗？"></a>Redis Cluster 扩容缩容期间可以提供服务吗？</h5><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">类似的问题：</span><br><span class="line">● 如果客户端访问的 key 所属的槽正在迁移怎么办？</span><br><span class="line">● 如何确定给定 key 的应该分布到哪个哈希槽中？</span><br></pre></td></tr></table></figure>

<p><strong>Redis Cluster 扩容和缩容本质是进⾏重新分⽚，动态迁移哈希槽。</strong></p>
<p>两种不同的类型： <font color='orange'>ASK 重定向</font>和<font color='orange'>MOVED 重定向</font></p>
<p>&#x3D;&#x3D;ASK 重定&#x3D;&#x3D;向是下⾯这样的： </p>
<ol>
<li>客户端发送请求命令，如果请求的 key 对应的哈希槽还在当前节点的话，就 直接响应客户端的请求。</li>
<li>如果客户端请求的 key 对应的哈希槽当前正在迁移⾄新的节点，就会返回 - ASK 重定向错误，告知客户端要将请求发送到哈希槽被迁移到的⽬标节点。</li>
<li>客户端收到 -ASK 重定向错误后，将会临时（⼀次性）重定向，⾃动向⽬标节点发送⼀条 ASKING 命令。也就是说，接收到 ASKING 命令的节点会强制执⾏⼀次请求，下次再来需要重新提前发送 ASKING 命令。</li>
<li>客户端发送真正的请求命令。 </li>
<li>ASK 重定向并不会同步更新客户端缓存的哈希槽分配信息，也就是说，客户端对正在迁移的相同哈希槽的请求依然会发送到原节点⽽不是⽬标节点。</li>
</ol>
<p><img src="https://img-blog.csdn.net/20180824153416344?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L25ld2JpZV85MDc0ODY4NTI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70" alt="这里写图片描述"></p>
<p>&#x3D;&#x3D;Moved重定向&#x3D;&#x3D;</p>
<p>如果客户端请求的 key 对应的哈希槽应该迁移完成的话，就会返回 -MOVED 重定向错误，告知客户端当前哈希槽是由哪个节点负责，客户端向⽬标节点发送请 求并更新缓存的哈希槽分配信息。</p>
<p><img src="https://upload-images.jianshu.io/upload_images/10007098-7c1a117c179541f4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1146/format/webp" alt="img"></p>
<h5 id="Redis-Cluster-中的节点是怎么进⾏通信的？"><a href="#Redis-Cluster-中的节点是怎么进⾏通信的？" class="headerlink" title="Redis Cluster 中的节点是怎么进⾏通信的？"></a>Redis Cluster 中的节点是怎么进⾏通信的？</h5><ul>
<li>Redis Cluster 中的各个节点基于Gossip协议来进⾏通信共享信息，每个Redis 节点都维护了⼀份集群的状态信息。</li>
<li>MEET ：在 Redis Cluster 中的某个 Redis 节点上执⾏ CLUSTER MEET ip port 命令，可以向指定的 Redis 节点发送⼀条 MEET 信息，⽤于将其添加 进 Redis Cluster 成为新的 Redis 节点。 、PING&#x2F;PONG ：Redis Cluster 中的节点都会定时地向其他节点发送 PING 消 息，来交换各个节点状态信息，检查各个节点状态，包括在线状态、疑似下 线状态 PFAIL 和已下线状态 FAIL。</li>
<li>FAIL ：Redis Cluster 中的节点 A 发现 B 节点 PFAIL ，并且在下线报告的有 效期限内集群中半数以上的节点将 B 节点标记为 PFAIL，节点 A 就会向集群 ⼴播⼀条 FAIL 消息，通知其他节点将故障节点 B 标记为 FAIL 。</li>
</ul>
<p>Redis Cluster 相当于是内置了 Sentinel 机制，Redis Cluster 内部的各个 Redis 节点通过 Gossip 协议互相探测健康状态，在&#x3D;&#x3D;故障时可以⾃动切换&#x3D;&#x3D;</p>
<hr>
<h4 id="重要知识点-4"><a href="#重要知识点-4" class="headerlink" title="重要知识点"></a>重要知识点</h4><h5 id="3种常用的缓存读写策略详解"><a href="#3种常用的缓存读写策略详解" class="headerlink" title="3种常用的缓存读写策略详解"></a>3种常用的缓存读写策略详解</h5><h6 id="Cache-Aside-Pattern（旁路缓存模式）"><a href="#Cache-Aside-Pattern（旁路缓存模式）" class="headerlink" title="Cache Aside Pattern（旁路缓存模式）"></a>Cache Aside Pattern（旁路缓存模式）</h6><p><strong>Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式，比较适合读请求比较多的场景。</strong></p>
<p><strong>写</strong> ：</p>
<ul>
<li>先更新 db</li>
<li>然后直接删除 cache 。</li>
</ul>
<p><strong>读</strong> :</p>
<ul>
<li>从 cache 中读取数据，读取到就直接返回</li>
<li>cache 中读取不到的话，就从 db 中读取数据返回</li>
<li>再把数据放到 cache 中。</li>
</ul>
<hr>
<h6 id="Read-x2F-Write-Through-Pattern（读写穿透）"><a href="#Read-x2F-Write-Through-Pattern（读写穿透）" class="headerlink" title="Read&#x2F;Write Through Pattern（读写穿透）"></a>Read&#x2F;Write Through Pattern（读写穿透）</h6><p><strong>写（Write Through）：</strong></p>
<ul>
<li>先查 cache，cache 中不存在，直接更新 db。</li>
<li>cache 中存在，则先更新 cache，然后 cache 服务自己更新 db（<strong>同步更新 cache 和 db</strong>）</li>
</ul>
<p><strong>读(Read Through)：</strong></p>
<ul>
<li>从 cache 中读取数据，读取到就直接返回 。</li>
<li>读取不到的话，先从 db 加载，写入到 cache 后返回响应。</li>
</ul>
<hr>
<h6 id="Write-Behind-Pattern（异步缓存写入）"><a href="#Write-Behind-Pattern（异步缓存写入）" class="headerlink" title="Write Behind Pattern（异步缓存写入）"></a>Write Behind Pattern（异步缓存写入）</h6><p>Write Behind Pattern 和 Read&#x2F;Write Through Pattern 很相似，两者都是由 cache 服务来负责 cache 和 db 的读写。但是，两个又有很大的不同：<strong>Read&#x2F;Write Through 是同步更新 cache 和 db，而 Write Behind 则是只更新缓存，不直接更新 db，而是改为异步批量的方式来更新 db。</strong></p>
<hr>
<h5 id="Redis-内存碎片详解"><a href="#Redis-内存碎片详解" class="headerlink" title="Redis 内存碎片详解"></a>Redis 内存碎片详解</h5><h6 id="为什么会有-Redis-内存碎片"><a href="#为什么会有-Redis-内存碎片" class="headerlink" title="为什么会有 Redis 内存碎片?"></a>为什么会有 Redis 内存碎片?</h6><p><strong>1、Redis 存储存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。</strong></p>
<p><strong>2、频繁修改 Redis 中的数据也会产生内存碎片。</strong></p>
<p>当 Redis 中的某个数据删除时，Redis 通常不会轻易释放内存给操作系统。</p>
<hr>
<h3 id="MongoDB"><a href="#MongoDB" class="headerlink" title="MongoDB"></a>MongoDB</h3><h4 id="MongoDB常见面试题总结（上）"><a href="#MongoDB常见面试题总结（上）" class="headerlink" title="MongoDB常见面试题总结（上）"></a>MongoDB常见面试题总结（上）</h4><h5 id="MongoDB-基础"><a href="#MongoDB-基础" class="headerlink" title="MongoDB 基础"></a>MongoDB 基础</h5><h6 id="MongoDB-是什么？"><a href="#MongoDB-是什么？" class="headerlink" title="MongoDB 是什么？"></a>MongoDB 是什么？</h6><p>MongoDB 是一个基于 <strong>分布式文件存储</strong> 的开源 NoSQL 数据库系统，由 <strong>C++</strong> 编写的。MongoDB 提供了 <strong>面向文档</strong> 的存储方式，操作起来比较简单和容易，支持“<strong>无模式</strong>”的数据建模，可以存储比较复杂的数据类型，是一款非常流行的 <strong>文档类型数据库</strong> 。</p>
<hr>
<h6 id="MongoDB-的存储结构是什么？"><a href="#MongoDB-的存储结构是什么？" class="headerlink" title="MongoDB 的存储结构是什么？"></a>MongoDB 的存储结构是什么？</h6><p>MongoDB 的存储结构区别于传统的关系型数据库，主要由如下三个单元组成：</p>
<ul>
<li><strong>文档（Document）</strong> ：MongoDB 中最基本的单元，由 BSON 键值对（key-value）组成，类似于关系型数据库中的行（Row）。</li>
<li><strong>集合（Collection）</strong> ：一个集合可以包含多个文档，类似于关系型数据库中的表（Table）。</li>
<li><strong>数据库（Database）</strong> ：一个数据库中可以包含多个集合，可以在 MongoDB 中创建多个数据库，类似于关系型数据库中的数据库（Database）。</li>
</ul>
<p><strong>SQL 与 MongoDB 常见术语对比</strong> ：</p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230308180003601.png" alt="image-20230308180003601"></p>
<hr>
<h6 id="MongoDB-有什么特点？"><a href="#MongoDB-有什么特点？" class="headerlink" title="MongoDB 有什么特点？"></a>MongoDB 有什么特点？</h6><ul>
<li><strong>数据记录被存储为文档</strong> ：MongoDB 中的记录就是一个 BSON 文档，它是由键值对组成的数据结构，类似于 JSON 对象，是 MongoDB 中的基本数据单元。</li>
<li><strong>模式自由</strong> ：集合的概念类似 MySQL 里的表，但它不需要定义任何模式，能够用更少的数据对象表现复杂的领域模型对象。</li>
<li><strong>支持多种查询方式</strong> ：MongoDB 查询 API 支持读写操作 (CRUD)以及数据聚合、文本搜索和地理空间查询。</li>
<li><strong>支持 ACID 事务</strong> ：NoSQL 数据库通常不支持事务，为了可扩展和高性能进行了权衡。不过，也有例外，MongoDB 就支持事务。与关系型数据库一样，MongoDB 事务同样具有 ACID 特性。MongoDB 单文档原生支持原子性，也具备事务的特性。MongoDB 4.0 加入了对多文档事务的支持，但只支持复制集部署模式下的事务，也就是说事务的作用域限制为一个副本集内。MongoDB 4.2 引入了分布式事务，增加了对分片集群上多文档事务的支持，并合并了对副本集上多文档事务的现有支持。</li>
<li><strong>高效的二进制存储</strong> ：存储在集合中的文档，是以键值对的形式存在的。键用于唯一标识一个文档，一般是 ObjectId 类型，值是以 BSON 形式存在的。BSON &#x3D; Binary JSON， 是在 JSON 基础上加了一些类型及元数据描述的格式。</li>
<li><strong>自带数据压缩功能</strong> ：存储同样的数据所需的资源更少。</li>
<li><strong>支持 mapreduce</strong> ：通过分治的方式完成复杂的聚合任务。不过，从 MongoDB 5.0 开始，map-reduce 已经不被官方推荐使用了，替代方案是 <a target="_blank" rel="noopener" href="https://www.mongodb.com/docs/manual/core/aggregation-pipeline/">聚合管道open in new window</a>。聚合管道提供比 map-reduce 更好的性能和可用性。</li>
<li><strong>支持多种类型的索引</strong> ：MongoDB 支持多种类型的索引，包括单字段索引、复合索引、多键索引、哈希索引、文本索引、 地理位置索引等，每种类型的索引有不同的使用场合。</li>
<li><strong>支持 failover</strong> ：提供自动故障恢复的功能，主节点发生故障时，自动从从节点中选举出一个新的主节点，确保集群的正常使用，这对于客户端来说是无感知的。</li>
<li><strong>支持分片集群</strong> ：MongoDB 支持集群自动切分数据，让集群存储更多的数据，具备更强的性能。在数据插入和更新时，能够自动路由和存储。</li>
<li><strong>支持存储大文件</strong> ：MongoDB 的单文档存储空间要求不超过 16MB。对于超过 16MB 的大文件，MongoDB 提供了 GridFS 来进行存储，通过 GridFS，可以将大型数据进行分块处理，然后将这些切分后的小文档保存在数据库中。</li>
</ul>
<hr>
<h6 id="MongoDB-适合什么应用场景？"><a href="#MongoDB-适合什么应用场景？" class="headerlink" title="MongoDB 适合什么应用场景？"></a>MongoDB 适合什么应用场景？</h6><p><strong>MongoDB 的优势在于其数据模型和存储引擎的灵活性、架构的可扩展性以及对强大的索引支持。</strong></p>
<h5 id="MongoDB-存储引擎"><a href="#MongoDB-存储引擎" class="headerlink" title="MongoDB 存储引擎"></a>MongoDB 存储引擎</h5><h6 id="MongoDB-存储引擎-1"><a href="#MongoDB-存储引擎-1" class="headerlink" title="MongoDB 存储引擎"></a>MongoDB 存储引擎</h6><p>存储引擎（Storage Engine）是数据库的核心组件，负责管理数据在内存和磁盘中的存储方式。</p>
<p>与 MySQL 一样，MongoDB 采用的也是 <strong>插件式的存储引擎架构</strong> </p>
<blockquote>
<p>插件式的存储引擎架构可以实现 Server 层和存储引擎层的解耦，可以支持多种存储引擎，如MySQL既可以支持B-Tree结构的InnoDB存储引擎，还可以支持LSM结构的RocksDB存储引擎。</p>
</blockquote>
<p>现在主要有下面这两种存储引擎：</p>
<ul>
<li><strong>WiredTiger 存储引擎</strong> ：自 MongoDB 3.2 以后，默认的存储引擎为 <a target="_blank" rel="noopener" href="https://www.mongodb.com/docs/manual/core/wiredtiger/">WiredTiger 存储引擎open in new window</a> 。非常适合大多数工作负载，建议用于新部署。WiredTiger 提供文档级并发模型、检查点和数据压缩（后文会介绍到）等功能。</li>
<li><strong>In-Memory 存储引擎</strong> ：<a target="_blank" rel="noopener" href="https://www.mongodb.com/docs/manual/core/inmemory/">In-Memory 存储引擎open in new window</a>在 MongoDB Enterprise 中可用。它不是将文档存储在磁盘上，而是将它们保留在内存中以获得更可预测的数据延迟。</li>
</ul>
<hr>
<h5 id="MongoDB-聚合"><a href="#MongoDB-聚合" class="headerlink" title="MongoDB 聚合"></a>MongoDB 聚合</h5><h6 id="MongoDB-聚合有什么用？"><a href="#MongoDB-聚合有什么用？" class="headerlink" title="MongoDB 聚合有什么用？"></a>MongoDB 聚合有什么用？</h6><p>实际项目中，我们经常需要将多个文档甚至是多个集合汇总到一起计算分析（比如求和、取最大值）并返回计算后的结果，这个过程被称为 <strong>聚合操作</strong> 。</p>
<h6 id="MongoDB-提供了哪几种执行聚合的方法？"><a href="#MongoDB-提供了哪几种执行聚合的方法？" class="headerlink" title="MongoDB 提供了哪几种执行聚合的方法？"></a>MongoDB 提供了哪几种执行聚合的方法？</h6><p>MongoDB 提供了两种执行聚合的方法：</p>
<ul>
<li><strong>聚合管道（Aggregation Pipeline）</strong> ：执行聚合操作的首选方法。</li>
<li><strong>单一目的聚合方法（Single purpose aggregation methods）</strong> ：也就是单一作用的聚合函数比如 <code>count()</code>、<code>distinct()</code>、<code>estimatedDocumentCount()</code>。</li>
</ul>
<p>每个管道的工作流程是：</p>
<ol>
<li>接受一系列原始数据文档</li>
<li>对这些文档进行一系列运算</li>
<li>结果文档输出给下一个阶段</li>
</ol>
<hr>
<h5 id="MongoDB-事务"><a href="#MongoDB-事务" class="headerlink" title="MongoDB 事务"></a>MongoDB 事务</h5><hr>
<h5 id="MongoDB-数据压缩"><a href="#MongoDB-数据压缩" class="headerlink" title="MongoDB 数据压缩"></a>MongoDB 数据压缩</h5><hr>
<h4 id="MongoDB常见面试题总结（下）"><a href="#MongoDB常见面试题总结（下）" class="headerlink" title="MongoDB常见面试题总结（下）"></a>MongoDB常见面试题总结（下）</h4><h5 id="MongoDB-索引"><a href="#MongoDB-索引" class="headerlink" title="MongoDB 索引"></a>MongoDB 索引</h5><h6 id="MongoDB-索引有什么用"><a href="#MongoDB-索引有什么用" class="headerlink" title="MongoDB 索引有什么用?"></a>MongoDB 索引有什么用?</h6><p>和关系型数据库类似，MongoDB 中也有索引。索引的目的主要是用来提高查询效率，如果没有索引的话，MongoDB 必须执行 <strong>集合扫描</strong> ，即扫描集合中的每个文档，以选择与查询语句匹配的文档。如果查询存在合适的索引，MongoDB 可以使用该索引来限制它必须检查的文档数量。并且，MongoDB 可以使用索引中的排序返回排序后的结果。</p>
<p>虽然索引可以显著缩短查询时间，但是使用索引、维护索引是有代价的。在执行写入操作时，除了要更新文档之外，还必须更新索引，这必然会影响写入的性能。因此，当有大量写操作而读操作少时，或者不考虑读操作的性能时，都不推荐建立索引。</p>
<hr>
<h6 id="MongoDB-支持哪些类型的索引？"><a href="#MongoDB-支持哪些类型的索引？" class="headerlink" title="MongoDB 支持哪些类型的索引？"></a>MongoDB 支持哪些类型的索引？</h6><p><strong>MongoDB 支持多种类型的索引，包括单字段索引、复合索引、多键索引、哈希索引、文本索引、 地理位置索引等，每种类型的索引有不同的使用场合。</strong></p>
<ul>
<li><strong>单字段索引：</strong> 建立在单个字段上的索引，索引创建的排序顺序无所谓，MongoDB 可以头&#x2F;尾开始遍历。</li>
<li><strong>复合索引：</strong> 建立在多个字段上的索引，也可以称之为组合索引、联合索引。</li>
<li><strong>多键索引</strong> ：MongoDB 的一个字段可能是数组，在对这种字段创建索引时，就是多键索引。MongoDB 会为数组的每个值创建索引。就是说你可以按照数组里面的值做条件来查询，这个时候依然会走索引。</li>
<li><strong>哈希索引</strong> ：按数据的哈希值索引，用在哈希分片集群上。</li>
<li><strong>文本索引：</strong> 支持对字符串内容的文本搜索查询。文本索引可以包含任何值为字符串或字符串元素数组的字段。一个集合只能有一个文本搜索索引，但该索引可以覆盖多个字段。MongoDB 虽然支持全文索引，但是性能低下，暂时不建议使用。</li>
<li><strong>地理位置索引：</strong> 基于经纬度的索引，适合 2D 和 3D 的位置查询。</li>
<li><strong>唯一索引</strong> ：确保索引字段不会存储重复值。如果集合已经存在了违反索引的唯一约束的文档，则后台创建唯一索引会失败。</li>
<li><strong>TTL 索引</strong> ：TTL 索引提供了一个过期机制，允许为每一个文档设置一个过期时间，当一个文档达到预设的过期时间之后就会被删除。</li>
</ul>
<hr>
<h5 id="MongoDB-高可用"><a href="#MongoDB-高可用" class="headerlink" title="MongoDB 高可用"></a>MongoDB 高可用</h5><h6 id="复制集群"><a href="#复制集群" class="headerlink" title="复制集群"></a>复制集群</h6><p><strong>什么是复制集群？</strong></p>
<p>MongoDB 的复制集群又称为副本集群，是一组维护相同数据集合的 mongod 进程。</p>
<p>客户端连接到整个 Mongodb 复制集群，主节点机负责整个复制集群的写，从节点可以进行读操作，但默认还是主节点负责整个复制集群的读。主节点发生故障时，自动从从节点中选举出一个新的主节点，确保集群的正常使用，这对于客户端来说是无感知的。</p>
<p><strong>主节点</strong> ：整个集群的写操作入口，接收所有的写操作，并将集合所有的变化记录到操作日志中，即 oplog。主节点挂掉之后会自动选出新的主节点。</p>
<p><strong>从节点</strong> ：从主节点同步数据，在主节点挂掉之后选举新节点。不过，从节点可以配置成 0 优先级，阻止它在选举中成为主节点。</p>
<p><strong>仲裁节点</strong> ：这个是为了节约资源或者多机房容灾用，只负责主节点选举时投票不存数据，保证能有节点获得多数赞成票。</p>
<p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/database/mongodb/replica-set-read-write-operations-primary.png" alt="img"></p>
<p>主节点与备节点之间是通过 <strong>oplog（操作日志）</strong> 来同步数据的。oplog 是 local 库下的一个特殊的 <strong>上限集合(Capped Collection)</strong> ，用来保存写操作所产生的增量日志，类似于 MySQL 中 的 Binlog。</p>
<blockquote>
<p>上限集合类似于定长的循环队列，数据顺序追加到集合的尾部，当集合空间达到上限时，它会覆盖集合中最旧的文档。上限集合的数据将会被顺序写入到磁盘的固定空间内，所以，I&#x2F;O 速度非常快，如果不建立索引，性能更好。</p>
</blockquote>
<p>当主节点上的一个写操作完成后，会向 oplog 集合写入一条对应的日志，而从节点则通过这个 oplog 不断拉取到新的日志，在本地进行回放以达到数据同步的目的。</p>
<p>副本集最多有一个主节点。 如果当前主节点不可用，一个选举会抉择出新的主节点。MongoDB 的节点选举规则能够保证在 Primary 挂掉之后选取的新节点一定是集群中数据最全的一个。</p>
<hr>
<p><strong>为什么要用复制集群？</strong></p>
<p> <strong>实现 failover</strong> ：提供自动故障恢复的功能，主节点发生故障时，自动从从节点中选举出一个新的主节点，确保集群的正常使用，这对于客户端来说是无感知的。</p>
<p><strong>实现读写分离</strong> ：我们可以设置从节点上可以读取数据，主节点负责写入数据，这样的话就实现了读写分离，减轻了主节点读写压力过大的问题。MongoDB 4.0 之前版本如果主库压力不大,不建议读写分离，因为写会阻塞读，除非业务对响应时间不是非常关注以及读取历史数据接受一定时间延迟。</p>
<hr>
<h6 id="分片集群"><a href="#分片集群" class="headerlink" title="分片集群"></a>分片集群</h6><p><strong>什么是分片集群？</strong></p>
<p>分片集群是 MongoDB 的分布式版本，相较副本集，分片集群数据被均衡的分布在不同分片中， 不仅大幅提升了整个集群的数据容量上限，也将读写的压力分散到不同分片，以解决副本集性能瓶颈的难题。</p>
<p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/database/mongodb/sharded-cluster-production-architecture.png" alt="img"></p>
<ul>
<li><strong>Config Servers</strong>：配置服务器，本质上是一个 MongoDB 的副本集，负责存储集群的各种元数据和配置，如分片地址、Chunks 等</li>
<li><strong>Mongos</strong>：路由服务，不存具体数据，从 Config 获取集群配置讲请求转发到特定的分片，并且整合分片结果返回给客户端。</li>
<li><strong>Shard</strong>：每个分片是整体数据的一部分子集，从MongoDB3.6版本开始，每个Shard必须部署为副本集（replica set）架构</li>
</ul>
<hr>
<p><strong>为什么要用分片集群？</strong></p>
<p>随着系统数据量以及吞吐量的增长，常见的解决办法有两种：<strong>垂直扩展和水平扩展</strong>。</p>
<p>垂直扩展通过增加单个服务器的能力来实现，比如磁盘空间、内存容量、CPU 数量等；水平扩展则通过将数据存储到多个服务器上来实现，根据需要添加额外的服务器以增加容量。</p>
<p>类似于 Redis Cluster，MongoDB 也可以通过分片实现 <strong>水平扩展</strong> 。水平扩展这种方式更灵活，可以满足更大数据量的存储需求，支持更高吞吐量。并且，水平扩展所需的整体成本更低，仅仅需要相对较低配置的单机服务器即可，代价是增加了部署的基础设施和维护的复杂性。</p>
<hr>
<p><strong>什么是分片键？</strong></p>
<p><strong>分片键（Shard Key）</strong> 是数据分区的前提， 从而实现数据分发到不同服务器上，减轻服务器的负担。也就是说，分片键决定了集合内的文档如何在集群的多个分片间的分布状况。</p>
<p>分片键就是文档里面的一个字段，但是这个字段不是普通的字段，有一定的要求：</p>
<ul>
<li>它必须在所有文档中都出现。</li>
<li>它必须是集合的一个索引，可以是单索引或复合索引的前缀索引，不能是多索引、文本索引或地理空间位置索引。</li>
<li>MongoDB 4.2 之前的版本，文档的分片键字段值不可变。MongoDB 4.2 版本开始，除非分片键字段是不可变的 <code>_id</code> 字段，否则您可以更新文档的分片键值。MongoDB 5.0 版本开始，实现了实时重新分片（live resharding），可以实现分片键的完全重新选择。</li>
<li>它的大小不能超过 512 字节。</li>
</ul>
<hr>
<p><strong>如何选择分片键？</strong></p>
<ul>
<li><strong>取值基数</strong> 取值基数建议尽可能大，如果用小基数的片键，因为备选值有限，那么块的总数量就有限，随着数据增多，块的大小会越来越大，导致水平扩展时移动块会非常困难。 例如：选择年龄做一个基数，范围最多只有100个，随着数据量增多，同一个值分布过多时，导致 chunck 的增长超出 chuncksize 的范围，引起 jumbo chunk，从而无法迁移，导致数据分布不均匀，性能瓶颈。</li>
<li><strong>取值分布</strong> 取值分布建议尽量均匀，分布不均匀的片键会造成某些块的数据量非常大，同样有上面数据分布不均匀，性能瓶颈的问题。</li>
<li><strong>查询带分片</strong> 查询时建议带上分片，使用分片键进行条件查询时，mongos 可以直接定位到具体分片，否则 mongos 需要将查询分发到所有分片，再等待响应返回。</li>
<li><strong>避免单调递增或递减</strong> 单调递增的 sharding key，数据文件挪动小，但写入会集中，导致最后一篇的数据量持续增大，不断发生迁移，递减同理。</li>
</ul>
<hr>
<p><strong>分片策略有哪些</strong>？</p>
<p><strong>1、基于范围的分片</strong> ：</p>
<p>MongoDB 按照分片键（Shard Key）的值的范围将数据拆分为不同的块（Chunk），每个块包含了一段范围内的数据。当分片键的基数大、频率低且值非单调变更时，范围分片更高效。</p>
<ul>
<li>优点： Mongos 可以快速定位请求需要的数据，并将请求转发到相应的 Shard 节点中。</li>
<li>缺点： 可能导致数据在 Shard 节点上分布不均衡，容易造成读写热点，且不具备写分散性。</li>
<li>适用场景：分片键的值不是单调递增或单调递减、分片键的值基数大且重复的频率低、需要范围查询等业务场景。</li>
</ul>
<p><strong>2、基于 Hash 值的分片</strong></p>
<p>MongoDB 计算单个字段的哈希值作为索引值，并以哈希值的范围将数据拆分为不同的块（Chunk）。</p>
<ul>
<li>优点：可以将数据更加均衡地分布在各 Shard 节点中，具备写分散性。</li>
<li>缺点：不适合进行范围查询，进行范围查询时，需要将读请求分发到所有的 Shard 节点。</li>
<li>适用场景：分片键的值存在单调递增或递减、片键的值基数大且重复的频率低、需要写入的数据随机分发、数据读取随机性较大等业务场景。</li>
</ul>
<hr>
<p><strong>分片数据如何存储？</strong></p>
<p><strong>Chunk（块）</strong> 是 MongoDB 分片集群的一个核心概念，其本质上就是由一组 Document 组成的逻辑数据单元。每个 Chunk 包含一定范围片键的数据，互不相交且并集为全部数据，即离散数学中<strong>划分</strong>的概念。</p>
<hr>
<h1 id="开发工具"><a href="#开发工具" class="headerlink" title="开发工具"></a>开发工具</h1><h2 id="Maven"><a href="#Maven" class="headerlink" title="Maven"></a>Maven</h2><h3 id="Maven-核心概念总结"><a href="#Maven-核心概念总结" class="headerlink" title="Maven 核心概念总结"></a>Maven 核心概念总结</h3><h2 id="Gradle"><a href="#Gradle" class="headerlink" title="Gradle"></a>Gradle</h2><h2 id="Git"><a href="#Git" class="headerlink" title="Git"></a>Git</h2><h2 id="Docker"><a href="#Docker" class="headerlink" title="Docker"></a>Docker</h2><h3 id="Docker-核心概念总结"><a href="#Docker-核心概念总结" class="headerlink" title="Docker 核心概念总结"></a>Docker 核心概念总结</h3><h4 id="认识容器"><a href="#认识容器" class="headerlink" title="认识容器"></a>认识容器</h4><h5 id="什么是容器"><a href="#什么是容器" class="headerlink" title="什么是容器?"></a>什么是容器?</h5><p>先来看看容器较为官方的解释</p>
<p><strong>一句话概括容器：容器就是将软件打包成标准化单元，以用于开发、交付和部署。</strong></p>
<ul>
<li><strong>容器镜像是轻量的、可执行的独立软件包</strong> ，包含软件运行所需的所有内容：代码、运行时环境、系统工具、系统库和设置。</li>
<li><strong>容器化软件适用于基于 Linux 和 Windows 的应用，在任何环境中都能够始终如一地运行。</strong></li>
<li><strong>容器赋予了软件独立性</strong>，使其免受外在环境差异（例如，开发和预演环境的差异）的影响，从而有助于减少团队间在相同基础设施上运行不同软件时的冲突。</li>
</ul>
<p><strong>再来看看容器较为通俗的解释</strong></p>
<p><strong>如果需要通俗地描述容器的话，我觉得容器就是一个存放东西的地方，就像书包可以装各种文具、衣柜可以放各种衣服、鞋架可以放各种鞋子一样。我们现在所说的容器存放的东西可能更偏向于应用比如网站、程序甚至是系统环境。</strong></p>
<hr>
<h5 id="图解物理机-虚拟机与容器"><a href="#图解物理机-虚拟机与容器" class="headerlink" title="图解物理机,虚拟机与容器"></a>图解物理机,虚拟机与容器</h5><p><strong>物理机：</strong></p>
<p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/%E7%89%A9%E7%90%86%E6%9C%BA%E5%9B%BE%E8%A7%A3.png" alt="物理机"></p>
<p><strong>虚拟机：</strong></p>
<p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%9B%BE%E8%A7%A3.png" alt="虚拟机"></p>
<p><strong>容器：</strong></p>
<p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/image-20211110104003678.png" alt="img"></p>
<p>通过上面这三张抽象图，我们可以大概通过类比概括出： <strong>容器虚拟化的是操作系统而不是硬件，容器之间是共享同一套操作系统资源的。虚拟机技术是虚拟出一套硬件后，在其上运行一个完整操作系统。因此容器的隔离级别会稍低一些。</strong></p>
<hr>
<h4 id="再来谈谈-Docker-的一些概念"><a href="#再来谈谈-Docker-的一些概念" class="headerlink" title="再来谈谈 Docker 的一些概念"></a>再来谈谈 Docker 的一些概念</h4><h5 id="什么是-Docker"><a href="#什么是-Docker" class="headerlink" title="什么是 Docker?"></a>什么是 Docker?</h5><p><strong>Docker 是世界领先的软件容器平台。</strong></p>
<ul>
<li><strong>Docker</strong> 使用 Google 公司推出的 <strong>Go 语言</strong> 进行开发实现，基于 <strong>Linux 内核</strong> 提供的 CGroup 功能和 namespace 来实现的，以及 AUFS 类的 <strong>UnionFS</strong> 等技术，<strong>对进程进行封装隔离，属于操作系统层面的虚拟化技术。</strong> 由于隔离的进程独立于宿主和其它的隔离的进程，因此也称其为容器。</li>
<li><strong>Docker 能够自动执行重复性任务，例如搭建和配置开发环境，从而解放了开发人员以便他们专注在真正重要的事情上：构建杰出的软件。</strong></li>
<li><strong>用户可以方便地创建和使用容器，把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改，就像管理普通的代码一样。</strong></li>
</ul>
<hr>
<h5 id="Docker-思想"><a href="#Docker-思想" class="headerlink" title="Docker 思想"></a>Docker 思想</h5><ul>
<li><strong>集装箱</strong></li>
<li><strong>标准化：</strong> ① 运输方式 ② 存储方式 ③ API 接口</li>
<li><strong>隔离</strong></li>
</ul>
<hr>
<h5 id="Docker-容器的特点"><a href="#Docker-容器的特点" class="headerlink" title="Docker 容器的特点"></a>Docker 容器的特点</h5><ul>
<li><strong>轻量</strong> : 在一台机器上运行的多个 Docker 容器可以共享这台机器的操作系统内核；它们能够迅速启动，只需占用很少的计算和内存资源。镜像是通过文件系统层进行构造的，并共享一些公共文件。这样就能尽量降低磁盘用量，并能更快地下载镜像。</li>
<li><strong>标准</strong> : Docker 容器基于开放式标准，能够在所有主流 Linux 版本、Microsoft Windows 以及包括 VM、裸机服务器和云在内的任何基础设施上运行。</li>
<li><strong>安全</strong> : Docker 赋予应用的隔离性不仅限于彼此隔离，还独立于底层的基础设施。Docker 默认提供最强的隔离，因此应用出现问题，也只是单个容器的问题，而不会波及到整台机器。</li>
</ul>
<hr>
<h5 id="为什么要用-Docker"><a href="#为什么要用-Docker" class="headerlink" title="为什么要用 Docker ?"></a>为什么要用 Docker ?</h5><ul>
<li><strong>Docker 的镜像提供了除内核外完整的运行时环境，确保了应用运行环境一致性，从而不会再出现 “这段代码在我机器上没问题啊” 这类问题；——一致的运行环境</strong></li>
<li><strong>可以做到秒级、甚至毫秒级的启动时间。大大的节约了开发、测试、部署的时间。——更快速的启动时间</strong></li>
<li><strong>避免公用的服务器，资源会容易受到其他用户的影响。——隔离性</strong></li>
<li><strong>善于处理集中爆发的服务器使用压力；——弹性伸缩，快速扩展</strong></li>
<li><strong>可以很轻易的将在一个平台上运行的应用，迁移到另一个平台上，而不用担心运行环境的变化导致应用无法正常运行的情况。——迁移方便</strong></li>
<li><strong>使用 Docker 可以通过定制应用镜像来实现持续集成、持续交付、部署。——持续交付和部署</strong></li>
</ul>
<hr>
<h4 id="容器-VS-虚拟机"><a href="#容器-VS-虚拟机" class="headerlink" title="容器 VS 虚拟机"></a>容器 VS 虚拟机</h4><p><strong>每当说起容器，我们不得不将其与虚拟机做一个比较。就我而言，对于两者无所谓谁会取代谁，而是两者可以和谐共存。</strong></p>
<p>简单来说： <strong>容器和虚拟机具有相似的资源隔离和分配优势，但功能有所不同，因为容器虚拟化的是操作系统，而不是硬件，因此容器更容易移植，效率也更高。</strong></p>
<h5 id="两者对比图"><a href="#两者对比图" class="headerlink" title="两者对比图"></a>两者对比图</h5><p>传统虚拟机技术是虚拟出一套硬件后，在其上运行一个完整操作系统，在该系统上再运行所需应用进程；而容器内的应用进程直接运行于宿主的内核，容器内没有自己的内核，而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。</p>
<p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/2e2b95eebf60b6d03f6c1476f4d7c697.png" alt="img"></p>
<h5 id="容器与虚拟机总结"><a href="#容器与虚拟机总结" class="headerlink" title="容器与虚拟机总结"></a>容器与虚拟机总结</h5><p><img src="E:\XxdBlog\source_posts\images\test\image-20230308210737879.png" alt="image-20230308210737879"></p>
<ul>
<li><strong>容器是一个应用层抽象，用于将代码和依赖资源打包在一起。</strong> <strong>多个容器可以在同一台机器上运行，共享操作系统内核，但各自作为独立的进程在用户空间中运行</strong> 。与虚拟机相比， <strong>容器占用的空间较少</strong>（容器镜像大小通常只有几十兆），<strong>瞬间就能完成启动</strong> 。</li>
<li><strong>虚拟机 (VM) 是一个物理硬件层抽象，用于将一台服务器变成多台服务器。</strong> 管理程序允许多个 VM 在一台机器上运行。每个 VM 都包含一整套操作系统、一个或多个应用、必要的二进制文件和库资源，因此 <strong>占用大量空间</strong> 。而且 VM <strong>启动也十分缓慢</strong> 。</li>
</ul>
<p><strong>虚拟机更擅长于彻底隔离整个运行环境</strong>。例如，云服务提供商通常采用虚拟机技术隔离不同的用户。而 <strong>Docker 通常用于隔离不同的应用</strong> ，例如前端，后端以及数据库。</p>
<hr>
<h5 id="容器与虚拟机两者是可以共存的"><a href="#容器与虚拟机两者是可以共存的" class="headerlink" title="容器与虚拟机两者是可以共存的"></a>容器与虚拟机两者是可以共存的</h5><p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/javaguide/056c87751b9dd7b56f4264240fe96d00.png" alt="img"></p>
<hr>
<h4 id="Docker-基本概念"><a href="#Docker-基本概念" class="headerlink" title="Docker 基本概念"></a>Docker 基本概念</h4><p><strong>Docker 中有非常重要的三个基本概念，理解了这三个概念，就理解了 Docker 的整个生命周期。</strong></p>
<ul>
<li><strong>镜像（Image）</strong></li>
<li><strong>容器（Container）</strong></li>
<li><strong>仓库（Repository）</strong></li>
</ul>
<hr>
<h5 id="镜像-Image-一个特殊的文件系统"><a href="#镜像-Image-一个特殊的文件系统" class="headerlink" title="镜像(Image):一个特殊的文件系统"></a>镜像(Image):一个特殊的文件系统</h5><p><strong>操作系统分为内核和用户空间</strong>。对于 Linux 而言，内核启动后，会挂载 root 文件系统为其提供用户空间支持。而 Docker 镜像（Image），就相当于是一个 root 文件系统。</p>
<p><strong>Docker 镜像是一个特殊的文件系统，除了提供容器运行时所需的程序、库、资源、配置等文件外，还包含了一些为运行时准备的一些配置参数（如匿名卷、环境变量、用户等）。</strong> 镜像不包含任何动态数据，其内容在构建之后也不会被改变。</p>
<p>Docker 设计为<strong>分层存储的架构</strong> 。镜像实际是由多层文件系统联合组成。</p>
<p><strong>镜像构建时，会一层层构建，前一层是后一层的基础。每一层构建完就不会再发生改变，后一层上的任何改变只发生在自己这一层。</strong> 比如，删除前一层文件的操作，实际不是真的删除前一层的文件，而是仅在当前层标记为该文件已删除。在最终容器运行的时候，虽然不会看到这个文件，但是实际上该文件会一直跟随镜像。</p>
<hr>
<h5 id="容器-Container-镜像运行时的实体"><a href="#容器-Container-镜像运行时的实体" class="headerlink" title="容器(Container):镜像运行时的实体"></a>容器(Container):镜像运行时的实体</h5><p>镜像（Image）和容器（Container）的关系，就像是面向对象程序设计中的 类 和 实例 一样，镜像是静态的定义，<strong>容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等</strong> 。</p>
<p><strong>容器的实质是进程，但与直接在宿主执行的进程不同，容器进程运行于属于自己的独立的 命名空间。前面讲过镜像使用的是分层存储，容器也是如此。</strong></p>
<p><strong>容器存储层的生存周期和容器一样，容器消亡时，容器存储层也随之消亡。因此，任何保存于容器存储层的信息都会随容器删除而丢失。</strong></p>
<p><strong>容器不应该向其存储层内写入任何数据</strong> ，容器存储层要保持无状态化。<strong>所有的文件写入操作，都应该使用数据卷（Volume）、或者绑定宿主目录</strong>，<strong>使用数据卷后，容器可以随意删除、重新 run ，数据却不会丢失。</strong></p>
<hr>
<h5 id="仓库-Repository-集中存放镜像文件的地方"><a href="#仓库-Repository-集中存放镜像文件的地方" class="headerlink" title="仓库(Repository):集中存放镜像文件的地方"></a>仓库(Repository):集中存放镜像文件的地方</h5><p><strong>如果需要在其它服务器上使用这个镜像，我们就需要一个集中的存储、分发镜像的服务，Docker Registry 就是这样的服务。</strong></p>
<p><strong>镜像仓库是 Docker 用来集中存放镜像文件的地方类似于我们之前常用的代码仓库。</strong></p>
<p><strong>一个仓库会包含同一个软件不同版本的镜像</strong>，而<strong>标签就常用于对应该软件的各个版本</strong> 。</p>
<hr>
<h4 id="Build-Ship-and-Run"><a href="#Build-Ship-and-Run" class="headerlink" title="Build Ship and Run"></a>Build Ship and Run</h4><ul>
<li><strong>Build（构建镜像）</strong> ： 镜像就像是集装箱包括文件以及运行环境等等资源。</li>
<li><strong>Ship（运输镜像）</strong> ：主机和仓库间运输，这里的仓库就像是超级码头一样。</li>
<li><strong>Run （运行镜像）</strong> ：运行的镜像就是一个容器，容器就是运行程序的地方。</li>
</ul>
<p><strong>Docker 运行过程也就是去仓库把镜像拉到本地，然后用一条命令把镜像运行起来变成容器。所以，我们也常常将 Docker 称为码头工人或码头装卸工，这和 Docker 的中文翻译搬运工人如出一辙。</strong></p>
<hr>
<h4 id="Docker-底层原理"><a href="#Docker-底层原理" class="headerlink" title="Docker 底层原理"></a>Docker 底层原理</h4><h5 id="虚拟化技术"><a href="#虚拟化技术" class="headerlink" title="虚拟化技术"></a>虚拟化技术</h5><p>虚拟化技术是一种资源管理技术，是将计算机的各种<a target="_blank" rel="noopener" href="https://zh.wikipedia.org/wiki/%E8%B3%87%E6%BA%90_(%E8%A8%88%E7%AE%97%E6%A9%9F%E7%A7%91%E5%AD%B8" title="实体资源">实体资源</a>)（<a target="_blank" rel="noopener" href="https://zh.wikipedia.org/wiki/CPU">CPUopen in new window</a>、<a target="_blank" rel="noopener" href="https://zh.wikipedia.org/wiki/%E5%86%85%E5%AD%98">内存open in new window</a>、<a target="_blank" rel="noopener" href="https://zh.wikipedia.org/wiki/%E7%A3%81%E7%9B%98%E7%A9%BA%E9%97%B4">磁盘空间open in new window</a>、<a target="_blank" rel="noopener" href="https://zh.wikipedia.org/wiki/%E7%B6%B2%E8%B7%AF%E9%81%A9%E9%85%8D%E5%99%A8">网络适配器open in new window</a>等），予以抽象、转换后呈现出来并可供分割、组合为一个或多个电脑配置环境。由此，打破实体结构间的不可切割的障碍，使用户可以比原本的配置更好的方式来应用这些电脑硬件资源。这些资源的新虚拟部分是不受现有资源的架设方式，地域或物理配置所限制。一般所指的虚拟化资源包括计算能力和数据存储。</p>
<hr>
<h5 id="Docker-基于-LXC-虚拟容器技术"><a href="#Docker-基于-LXC-虚拟容器技术" class="headerlink" title="Docker 基于 LXC 虚拟容器技术"></a>Docker 基于 LXC 虚拟容器技术</h5><p><strong>cgroup 和 namespace 介绍：</strong></p>
<ul>
<li><strong>namespace 是 Linux 内核用来隔离内核资源的方式。</strong> </li>
<li><strong>CGroup 是 Control Groups 的缩写，是 Linux 内核提供的一种可以限制、记录、隔离进程组 (process groups) 所使用的物力资源 (如 cpu memory i&#x2F;o 等等) 的机制。</strong></li>
</ul>
<p><strong>cgroup 和 namespace 两者对比：</strong></p>
<p>两者都是将进程进行分组，但是两者的作用还是有本质区别。namespace 是为了隔离进程组之间的资源，而 cgroup 是为了对一组进程进行统一的资源监控和限制。</p>
<hr>
<h1 id="常用框架"><a href="#常用框架" class="headerlink" title="常用框架"></a>常用框架</h1><h2 id="Spring-amp-Spring-Boot"><a href="#Spring-amp-Spring-Boot" class="headerlink" title="Spring&amp;Spring Boot"></a>Spring&amp;Spring Boot</h2><h3 id="Spring-常见面试题总结"><a href="#Spring-常见面试题总结" class="headerlink" title="Spring 常见面试题总结"></a>Spring 常见面试题总结</h3><h4 id="Spring-基础"><a href="#Spring-基础" class="headerlink" title="Spring 基础"></a>Spring 基础</h4><h5 id="什么是-Spring-框架"><a href="#什么是-Spring-框架" class="headerlink" title="什么是 Spring 框架?"></a>什么是 Spring 框架?</h5><p>Spring 提供的核心功能主要是 &#x3D;&#x3D;IoC（Inversion of Control:控制反转） 和 AOP(Aspect-Oriented Programming:面向切面编程)&#x3D;&#x3D;。</p>
<h5 id="Spring-包含的模块有哪些？"><a href="#Spring-包含的模块有哪些？" class="headerlink" title="Spring 包含的模块有哪些？"></a>Spring 包含的模块有哪些？</h5><h6 id="Core-Container"><a href="#Core-Container" class="headerlink" title="Core Container"></a>Core Container</h6><p>Spring 框架的核心模块，也可以说是基础模块，主要提供 IoC 依赖注入功能的支持。Spring 其他所有的功能基本都需要依赖于该模块，我们从上面那张 Spring 各个模块的依赖关系图就可以看出来。</p>
<ul>
<li><strong>spring-core</strong> ：Spring 框架基本的核心工具类。</li>
<li><strong>spring-beans</strong> ：提供对 bean 的创建、配置和管理等功能的支持。</li>
<li><strong>spring-context</strong> ：提供对国际化、事件传播、资源加载等功能的支持。</li>
<li><strong>spring-expression</strong> ：提供对表达式语言（Spring Expression Language） SpEL 的支持，只依赖于 core 模块，不依赖于其他模块，可以单独使用。</li>
</ul>
<hr>
<h5 id="Spring-Spring-MVC-Spring-Boot-之间什么关系"><a href="#Spring-Spring-MVC-Spring-Boot-之间什么关系" class="headerlink" title="Spring,Spring MVC,Spring Boot 之间什么关系?"></a>Spring,Spring MVC,Spring Boot 之间什么关系?</h5><p>Spring MVC 是 Spring 中的一个很重要的模块，主要赋予 Spring 快速构建 MVC 架构的 Web 程序的能力。MVC 是模型(Model)、视图(View)、控制器(Controller)的简写，其核心思想是通过将业务逻辑、数据、显示分离来组织代码。</p>
<p>使用 Spring 进行开发各种配置过于麻烦比如开启某些 Spring 特性时，需要用 XML 或 Java 进行显式配置。于是，Spring Boot 诞生了！</p>
<p>Spring 旨在简化 J2EE 企业应用程序开发。Spring Boot 旨在简化 Spring 开发（减少配置文件，开箱即用！）。</p>
<p>Spring Boot 只是简化了配置，如果你需要构建 MVC 架构的 Web 程序，你还是需要使用 Spring MVC 作为 MVC 框架，只是说 Spring Boot 帮你简化了 Spring MVC 的很多配置，真正做到开箱即用！</p>
<hr>
<h4 id="Spring-IoC"><a href="#Spring-IoC" class="headerlink" title="Spring IoC"></a>Spring IoC</h4><h5 id="谈谈自己对于-Spring-IoC-的了解"><a href="#谈谈自己对于-Spring-IoC-的了解" class="headerlink" title="谈谈自己对于 Spring IoC 的了解"></a>谈谈自己对于 Spring IoC 的了解</h5><p><strong>为什么叫控制反转？</strong></p>
<ul>
<li><strong>控制</strong> ：指的是对象创建（实例化、管理）的权力</li>
<li><strong>反转</strong> ：控制权交给外部环境（Spring 框架、IoC 容器）</li>
</ul>
<hr>
<h5 id="什么是-Spring-Bean？"><a href="#什么是-Spring-Bean？" class="headerlink" title="什么是 Spring Bean？"></a>什么是 Spring Bean？</h5><p>Bean 代指的就是那些被 IoC 容器所管理的对象。</p>
<hr>
<h5 id="将一个类声明为-Bean-的注解有哪些"><a href="#将一个类声明为-Bean-的注解有哪些" class="headerlink" title="将一个类声明为 Bean 的注解有哪些?"></a>将一个类声明为 Bean 的注解有哪些?</h5><ul>
<li><code>@Component</code> ：通用的注解，可标注任意类为 <code>Spring</code> 组件。如果一个 Bean 不知道属于哪个层，可以使用<code>@Component</code> 注解标注。</li>
<li><code>@Repository</code> : 对应持久层即 Dao 层，主要用于数据库相关操作。</li>
<li><code>@Service</code> : 对应服务层，主要涉及一些复杂的逻辑，需要用到 Dao 层。</li>
<li><code>@Controller</code> : 对应 Spring MVC 控制层，主要用户接受用户请求并调用 Service 层返回数据给前端页面。</li>
</ul>
<hr>
<h5 id="Component-和-Bean-的区别是什么？"><a href="#Component-和-Bean-的区别是什么？" class="headerlink" title="@Component 和 @Bean 的区别是什么？"></a>@Component 和 @Bean 的区别是什么？</h5><ul>
<li><code>@Component</code> 注解作用于类，而<code>@Bean</code>注解作用于方法。</li>
<li><code>@Component</code>通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中（我们可以使用 <code>@ComponentScan</code> 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中）。<code>@Bean</code> 注解通常是我们在标有该注解的方法中定义产生这个 bean,<code>@Bean</code>告诉了 Spring 这是某个类的实例，当我需要用它的时候还给我。</li>
<li><code>@Bean</code> 注解比 <code>@Component</code> 注解的自定义性更强，而且很多地方我们只能通过 <code>@Bean</code> 注解来注册 bean。比如当我们引用第三方库中的类需要装配到 <code>Spring</code>容器时，则只能通过 <code>@Bean</code>来实现。</li>
</ul>
<hr>
<h5 id="注入-Bean-的注解有哪些？"><a href="#注入-Bean-的注解有哪些？" class="headerlink" title="注入 Bean 的注解有哪些？"></a>注入 Bean 的注解有哪些？</h5><hr>
<h5 id="Autowired-和-Resource-的区别是什么？"><a href="#Autowired-和-Resource-的区别是什么？" class="headerlink" title="@Autowired 和 @Resource 的区别是什么？"></a>@Autowired 和 @Resource 的区别是什么？</h5><p>简单总结一下：</p>
<ul>
<li><code>@Autowired</code> 是 Spring 提供的注解，<code>@Resource</code> 是 JDK 提供的注解。</li>
<li><code>Autowired</code> 默认的注入方式为<code>byType</code>（根据类型进行匹配），<code>@Resource</code>默认注入方式为 <code>byName</code>（根据名称进行匹配）。</li>
<li>当一个接口存在多个实现类的情况下，<code>@Autowired</code> 和<code>@Resource</code>都需要通过名称才能正确匹配到对应的 Bean。<code>Autowired</code> 可以通过 <code>@Qualifier</code> 注解来显式指定名称，<code>@Resource</code>可以通过 <code>name</code> 属性来显式指定名称。</li>
</ul>
<hr>
<h4 id="Spring-AoP"><a href="#Spring-AoP" class="headerlink" title="Spring AoP"></a>Spring AoP</h4><h5 id="谈谈自己对于-AOP-的了解"><a href="#谈谈自己对于-AOP-的了解" class="headerlink" title="谈谈自己对于 AOP 的了解"></a>谈谈自己对于 AOP 的了解</h5><p>AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关，却为业务模块所共同调用的逻辑或责任（例如事务处理、日志管理、权限控制等）封装起来，便于减少系统的重复代码，降低模块间的耦合度，并有利于未来的可拓展性和可维护性。</p>
<hr>
<h4 id="Spring-MVC"><a href="#Spring-MVC" class="headerlink" title="Spring MVC"></a>Spring MVC</h4><h5 id="Spring-MVC-的核心组件有哪些？"><a href="#Spring-MVC-的核心组件有哪些？" class="headerlink" title="Spring MVC 的核心组件有哪些？"></a>Spring MVC 的核心组件有哪些？</h5><ul>
<li><strong><code>DispatcherServlet</code></strong> ：<strong>核心的中央处理器</strong>，负责接收请求、分发，并给予客户端响应。</li>
<li><strong><code>HandlerMapping</code></strong> ：<strong>处理器映射器</strong>，根据 uri 去匹配查找能处理的 <code>Handler</code> ，并会将请求涉及到的拦截器和 <code>Handler</code> 一起封装。</li>
<li><strong><code>HandlerAdapter</code></strong> ：<strong>处理器适配器</strong>，根据 <code>HandlerMapping</code> 找到的 <code>Handler</code> ，适配执行对应的 <code>Handler</code>；</li>
<li><strong><code>Handler</code></strong> ：<strong>请求处理器</strong>，处理实际请求的处理器。</li>
<li><strong><code>ViewResolver</code></strong> ：<strong>视图解析器</strong>，根据 <code>Handler</code> 返回的逻辑视图 &#x2F; 视图，解析并渲染真正的视图，并传递给 <code>DispatcherServlet</code> 响应客户端</li>
</ul>
<hr>
<h5 id="Spring-MVC-的核心组件有哪些？-1"><a href="#Spring-MVC-的核心组件有哪些？-1" class="headerlink" title="Spring MVC 的核心组件有哪些？"></a>Spring MVC 的核心组件有哪些？</h5><ul>
<li><strong><code>DispatcherServlet</code></strong> ：<strong>核心的中央处理器</strong>，负责接收请求、分发，并给予客户端响应。</li>
<li><strong><code>HandlerMapping</code></strong> ：<strong>处理器映射器</strong>，根据 uri 去匹配查找能处理的 <code>Handler</code> ，并会将请求涉及到的拦截器和 <code>Handler</code> 一起封装。</li>
<li><strong><code>HandlerAdapter</code></strong> ：<strong>处理器适配器</strong>，根据 <code>HandlerMapping</code> 找到的 <code>Handler</code> ，适配执行对应的 <code>Handler</code>；</li>
<li><strong><code>Handler</code></strong> ：<strong>请求处理器</strong>，处理实际请求的处理器。</li>
<li><strong><code>ViewResolver</code></strong> ：<strong>视图解析器</strong>，根据 <code>Handler</code> 返回的逻辑视图 &#x2F; 视图，解析并渲染真正的视图，并传递给 <code>DispatcherServlet</code> 响应客户端</li>
</ul>
<hr>
<h5 id="SpringMVC-工作原理了解吗"><a href="#SpringMVC-工作原理了解吗" class="headerlink" title="SpringMVC 工作原理了解吗?"></a>SpringMVC 工作原理了解吗?</h5><p><img src="https://img-blog.csdnimg.cn/img_convert/de6d2b213f112297298f3e223bf08f28.png" alt="img"></p>
<p>&#x3D;&#x3D;<strong>流程说明（重要）：</strong>&#x3D;&#x3D;</p>
<ol>
<li>客户端（浏览器）发送请求， <code>DispatcherServlet</code>拦截请求。</li>
<li><code>DispatcherServlet</code> 根据请求信息调用 <code>HandlerMapping</code> 。<code>HandlerMapping</code> 根据 uri 去匹配查找能处理的 <code>Handler</code>（也就是我们平常说的 <code>Controller</code> 控制器） ，并会将请求涉及到的拦截器和 <code>Handler</code> 一起封装。</li>
<li><code>DispatcherServlet</code> 调用 <code>HandlerAdapter</code>适配执行 <code>Handler</code> 。</li>
<li><code>Handler</code> 完成对用户请求的处理后，会返回一个 <code>ModelAndView</code> 对象给<code>DispatcherServlet</code>，<code>ModelAndView</code> 顾名思义，包含了数据模型以及相应的视图的信息。<code>Model</code> 是返回的数据对象，<code>View</code> 是个逻辑上的 <code>View</code>。</li>
<li><code>ViewResolver</code> 会根据逻辑 <code>View</code> 查找实际的 <code>View</code>。</li>
<li><code>DispaterServlet</code> 把返回的 <code>Model</code> 传给 <code>View</code>（视图渲染）。</li>
<li>把 <code>View</code> 返回给请求者（浏览器）</li>
</ol>
<hr>
<h5 id="统一异常处理怎么做？"><a href="#统一异常处理怎么做？" class="headerlink" title="统一异常处理怎么做？"></a>统一异常处理怎么做？</h5><p>推荐使用注解的方式统一异常处理，具体会使用到 <code>@ControllerAdvice</code> + <code>@ExceptionHandler</code> 这两个注解 。</p>
<p><strong><code>getMappedMethod()</code>会首先找到可以匹配处理异常的所有方法信息，然后对其进行从小到大的排序，最后取最小的那一个匹配的方法(即匹配度最高的那个)。</strong></p>
<h4 id="Spring-框架中用到了哪些设计模式？"><a href="#Spring-框架中用到了哪些设计模式？" class="headerlink" title="Spring 框架中用到了哪些设计模式？"></a>Spring 框架中用到了哪些设计模式？</h4><ul>
<li><strong>工厂设计模式</strong> : Spring 使用工厂模式通过 <code>BeanFactory</code>、<code>ApplicationContext</code> 创建 bean 对象。</li>
<li><strong>代理设计模式</strong> : Spring AOP 功能的实现。</li>
<li><strong>单例设计模式</strong> : Spring 中的 Bean 默认都是单例的。</li>
<li><strong>模板方法模式</strong> : Spring 中 <code>jdbcTemplate</code>、<code>hibernateTemplate</code> 等以 Template 结尾的对数据库操作的类，它们就使用到了模板模式。</li>
<li><strong>包装器设计模式</strong> : 我们的项目需要连接多个数据库，而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。</li>
<li><strong>观察者模式:</strong> Spring 事件驱动模型就是观察者模式很经典的一个应用。</li>
<li><strong>适配器模式</strong> : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配<code>Controller</code>。</li>
</ul>
<hr>
<h4 id="Spring-事务"><a href="#Spring-事务" class="headerlink" title="Spring 事务"></a>Spring 事务</h4><h5 id="Spring-管理事务的方式有几种？"><a href="#Spring-管理事务的方式有几种？" class="headerlink" title="Spring 管理事务的方式有几种？"></a>Spring 管理事务的方式有几种？</h5><ul>
<li><strong>编程式事务</strong> ： 在代码中硬编码(不推荐使用) : 通过 <code>TransactionTemplate</code>或者 <code>TransactionManager</code> 手动管理事务，实际应用中很少使用，但是对于你理解 Spring 事务管理原理有帮助。</li>
<li><strong>声明式事务</strong> ： 在 XML 配置文件中配置或者直接基于注解（推荐使用） : 实际是通过 AOP 实现（基于<code>@Transactional</code> 的全注解方式使用最多）</li>
</ul>
<h5 id="Spring-事务中哪几种事务传播行为"><a href="#Spring-事务中哪几种事务传播行为" class="headerlink" title="Spring 事务中哪几种事务传播行为?"></a>Spring 事务中哪几种事务传播行为?</h5><p><strong>事务传播行为是为了解决业务层方法之间互相调用的事务问题</strong>。</p>
<hr>
<h4 id="Spring-Data-JPA"><a href="#Spring-Data-JPA" class="headerlink" title="Spring Data JPA"></a>Spring Data JPA</h4><hr>
<h3 id="重要知识点-5"><a href="#重要知识点-5" class="headerlink" title="重要知识点"></a>重要知识点</h3><h4 id="Spring-事务详解"><a href="#Spring-事务详解" class="headerlink" title="Spring 事务详解"></a>Spring 事务详解</h4><h5 id="什么是事务？"><a href="#什么是事务？" class="headerlink" title="什么是事务？"></a>什么是事务？</h5><p><strong>事务是逻辑上的一组操作，要么都执行，要么都不执行。</strong></p>
<p><strong>事务能否生效数据库引擎是否支持事务是关键。比如常用的 MySQL 数据库默认使用支持事务的 <code>innodb</code>引擎。但是，如果把数据库引擎变为 <code>myisam</code>，那么程序也就不再支持事务了！</strong></p>
<hr>
<h5 id="事务的特性（ACID）了解么"><a href="#事务的特性（ACID）了解么" class="headerlink" title="事务的特性（ACID）了解么?"></a>事务的特性（ACID）了解么?</h5><hr>
<h6 id="Spring-事务管理接口介绍"><a href="#Spring-事务管理接口介绍" class="headerlink" title="Spring 事务管理接口介绍"></a>Spring 事务管理接口介绍</h6><p>Spring 框架中，事务管理相关最重要的 3 个接口如下：</p>
<ul>
<li>**<code>PlatformTransactionManager</code>**： （平台）事务管理器，Spring 事务策略的核心。</li>
<li>**<code>TransactionDefinition</code>**： 事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。</li>
<li>**<code>TransactionStatus</code>**： 事务运行状态。</li>
</ul>
<hr>
<h6 id="TransactionDefinition-事务属性"><a href="#TransactionDefinition-事务属性" class="headerlink" title="TransactionDefinition:事务属性"></a>TransactionDefinition:事务属性</h6><p><strong>什么是事务属性呢？</strong> 事务属性可以理解成事务的一些基本配置，描述了事务策略如何应用到方法上。</p>
<p>事务属性包含了 5 个方面：</p>
<ul>
<li>隔离级别</li>
<li>传播行为</li>
<li>回滚规则</li>
<li>是否只读</li>
<li>事务超时</li>
</ul>
<hr>
<h4 id="SpringBoot-自动装配原理详解"><a href="#SpringBoot-自动装配原理详解" class="headerlink" title="SpringBoot 自动装配原理详解"></a>SpringBoot 自动装配原理详解</h4><p><strong>为什么 Spring Boot 使用起来这么酸爽呢？</strong> 这得益于其自动装配。<strong>自动装配可以说是 Spring Boot 的核心，那究竟什么是自动装配呢？</strong></p>
<h5 id="什么是-SpringBoot-自动装配？"><a href="#什么是-SpringBoot-自动装配？" class="headerlink" title="什么是 SpringBoot 自动装配？"></a>什么是 SpringBoot 自动装配？</h5><p><strong>通过注解或者一些简单的配置就能在 Spring Boot 的帮助下实现某块功能。</strong></p>
<h5 id="SpringBoot-是如何实现自动装配的？"><a href="#SpringBoot-是如何实现自动装配的？" class="headerlink" title="SpringBoot 是如何实现自动装配的？"></a>SpringBoot 是如何实现自动装配的？</h5><p>大概可以把 <code>@SpringBootApplication</code>看作是 <code>@Configuration</code>、<code>@EnableAutoConfiguration</code>、<code>@ComponentScan</code> 注解的集合。根据 SpringBoot 官网，这三个注解的作用分别是：</p>
<ul>
<li><code>@EnableAutoConfiguration</code>：启用 SpringBoot 的自动配置机制</li>
<li><code>@Configuration</code>：允许在上下文中注册额外的 bean 或导入其他配置类</li>
<li><code>@ComponentScan</code>： 扫描被<code>@Component</code> (<code>@Service</code>,<code>@Controller</code>)注解的 bean，注解默认会扫描启动类所在的包下所有的类 ，可以自定义不扫描某些 bean。如下图所示，容器中将排除<code>TypeExcludeFilter</code>和<code>AutoConfigurationExcludeFilter</code>。</li>
</ul>
<hr>
<h6 id="EnableAutoConfiguration-实现自动装配的核心注解"><a href="#EnableAutoConfiguration-实现自动装配的核心注解" class="headerlink" title="@EnableAutoConfiguration:实现自动装配的核心注解"></a>@EnableAutoConfiguration:实现自动装配的核心注解</h6><hr>
<h2 id="MyBatis常见面试题总结"><a href="#MyBatis常见面试题总结" class="headerlink" title="MyBatis常见面试题总结"></a>MyBatis常见面试题总结</h2><h3 id="Dao-接口的工作原理是什么？Dao-接口里的方法，参数不同时，方法能重载吗？"><a href="#Dao-接口的工作原理是什么？Dao-接口里的方法，参数不同时，方法能重载吗？" class="headerlink" title="Dao 接口的工作原理是什么？Dao 接口里的方法，参数不同时，方法能重载吗？"></a>Dao 接口的工作原理是什么？Dao 接口里的方法，参数不同时，方法能重载吗？</h3><p>Dao 接口就是人们常说的 <code>Mapper</code> 接口，接口的全限名，就是映射文件中的 namespace 的值，接口的方法名，就是映射文件中 <code>MappedStatement</code> 的 id 值，接口方法内的参数，就是传递给 sql 的参数。<strong>Mybatis 的 Dao 接口可以有多个重载方法，但是多个接口对应的映射必须只有一个，否则启动会报错。</strong></p>
<p>Dao 接口方法可以重载，但是需要满足以下条件：</p>
<ol>
<li>仅有一个无参方法和一个有参方法</li>
<li>多个有参方法时，参数数量必须一致。且使用相同的 <code>@Param</code> ，或者使用 <code>param1</code> 这种</li>
</ol>
<hr>
<h2 id="Netty"><a href="#Netty" class="headerlink" title="Netty"></a>Netty</h2><h3 id="x3D-x3D-Netty经典32问-x3D-x3D"><a href="#x3D-x3D-Netty经典32问-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;Netty经典32问&#x3D;&#x3D;"></a>&#x3D;&#x3D;Netty经典32问&#x3D;&#x3D;</h3><p><a target="_blank" rel="noopener" href="https://www.bilibili.com/read/cv22924686/#:~:text=%E5%9C%A8%20Netty%20%E4%B8%AD%EF%BC%8C%20Channel%20%E4%BB%A3%E8%A1%A8%E4%B8%80%E4%B8%AA%E5%BC%80%E6%94%BE%E7%9A%84%E7%BD%91%E7%BB%9C%E8%BF%9E%E6%8E%A5%EF%BC%8C%E5%AE%83%E5%8F%AF%E4%BB%A5%E7%94%A8%E6%9D%A5%E8%AF%BB%E5%8F%96%E5%92%8C%E5%86%99%E5%85%A5%E6%95%B0%E6%8D%AE%E3%80%82%20%E8%80%8C%20EventLoop%20%E5%88%99%E4%BB%A3%E8%A1%A8%E4%B8%80%E4%B8%AA%E6%89%A7%E8%A1%8C%E4%BB%BB%E5%8A%A1%E7%9A%84%E7%BA%BF%E7%A8%8B%EF%BC%8C%E5%AE%83%E8%B4%9F%E8%B4%A3%E5%A4%84%E7%90%86,Channel%20%E9%83%BD%E4%B8%8E%E4%B8%80%E4%B8%AA%20EventLoop%20%E5%85%B3%E8%81%94%EF%BC%8C%E8%80%8C%E4%B8%80%E4%B8%AA%20EventLoop%20%E5%8F%AF%E4%BB%A5%E5%85%B3%E8%81%94%E5%A4%9A%E4%B8%AA%20Channel%20%E3%80%82">https://www.bilibili.com/read/cv22924686/#:~:text=%E5%9C%A8%20Netty%20%E4%B8%AD%EF%BC%8C%20Channel%20%E4%BB%A3%E8%A1%A8%E4%B8%80%E4%B8%AA%E5%BC%80%E6%94%BE%E7%9A%84%E7%BD%91%E7%BB%9C%E8%BF%9E%E6%8E%A5%EF%BC%8C%E5%AE%83%E5%8F%AF%E4%BB%A5%E7%94%A8%E6%9D%A5%E8%AF%BB%E5%8F%96%E5%92%8C%E5%86%99%E5%85%A5%E6%95%B0%E6%8D%AE%E3%80%82%20%E8%80%8C%20EventLoop%20%E5%88%99%E4%BB%A3%E8%A1%A8%E4%B8%80%E4%B8%AA%E6%89%A7%E8%A1%8C%E4%BB%BB%E5%8A%A1%E7%9A%84%E7%BA%BF%E7%A8%8B%EF%BC%8C%E5%AE%83%E8%B4%9F%E8%B4%A3%E5%A4%84%E7%90%86,Channel%20%E9%83%BD%E4%B8%8E%E4%B8%80%E4%B8%AA%20EventLoop%20%E5%85%B3%E8%81%94%EF%BC%8C%E8%80%8C%E4%B8%80%E4%B8%AA%20EventLoop%20%E5%8F%AF%E4%BB%A5%E5%85%B3%E8%81%94%E5%A4%9A%E4%B8%AA%20Channel%20%E3%80%82</a></p>
<h3 id="BIO，NIO和AIO有啥区别"><a href="#BIO，NIO和AIO有啥区别" class="headerlink" title="BIO，NIO和AIO有啥区别"></a>BIO，NIO和AIO有啥区别</h3><p><img src="E:\XxdBlog\source_posts\images\test\image-20230407142216990.png" alt="image-20230407142216990"></p>
<p><img src="https://s2.loli.net/2023/04/07/2aYzBoNJCfysLmQ.png"></p>
<ul>
<li>BIO (Blocking I&#x2F;O): 同步阻塞 I&#x2F;O 模式</li>
<li>NIO (Non-blocking&#x2F;New I&#x2F;O): NIO 是⼀种同步⾮阻塞的 I&#x2F;O 模型。Channel , Selector ， Buffer。&#x3D;&#x3D;支持面向缓存的，基于通道的I&#x2F;O操作方式。&#x3D;&#x3D;</li>
<li>AIO (Asynchronous I&#x2F;O): AIO 也就是 NIO 2。基于事件和回调机制实现的。</li>
</ul>
<hr>
<h3 id="Netty-是什么？（Netty定义）"><a href="#Netty-是什么？（Netty定义）" class="headerlink" title="Netty 是什么？（Netty定义）"></a>Netty 是什么？（Netty定义）</h3><ol>
<li>Netty 是⼀个 基于 NIO 的 client-server(客户端服务器)框架，使⽤它可以快速简 单地开发⽹络应⽤程序。</li>
<li>它极⼤地简化并优化了 TCP 和 UDP 套接字服务器等⽹络编程,并且性能以及安全 性等很多⽅⾯甚⾄都要更好。</li>
<li>⽀持多种协议 如 FTP，SMTP，HTTP 以及各种⼆进制和基于⽂本的传统协议。</li>
</ol>
<p>Netty 成功地找到了⼀种在不妥协可维护性和性能的情况下实现易 于开发，性能，稳定性和灵活性的⽅法。</p>
<hr>
<h3 id="为什么要⽤-Netty？"><a href="#为什么要⽤-Netty？" class="headerlink" title="为什么要⽤ Netty？"></a>为什么要⽤ Netty？</h3><p>NIO 在⾯对断连重连、包丢失、粘包等问题时处理过程⾮常复杂。Netty 的出现 正是为了解决这些问题。</p>
<ul>
<li>统⼀的 API，⽀持多种传输类型，阻塞和⾮阻塞的。</li>
<li>简单⽽强⼤的线程模型。 </li>
<li>⾃带编解码器解决 TCP 粘包&#x2F;拆包问题。 </li>
<li>⾃带各种协议栈。</li>
<li>真正的⽆连接数据包套接字⽀持。 </li>
<li>⽐直接使⽤ Java 核⼼ API 有更⾼的吞吐量、更低的延迟、更低的资源消耗和更少的内存复制。 </li>
<li>安全性不错，有完整的 SSL&#x2F;TLS 以及 StartTLS ⽀持。 </li>
<li>社区活跃</li>
<li>成熟稳定，经历了⼤型项⽬的使⽤和考验，⽽且很多开源项⽬都使⽤到了 Netty， ⽐如我们经常接触的 Dubbo、RocketMQ 等等。</li>
</ul>
<hr>
<h3 id="Netty-应⽤场景了解么？"><a href="#Netty-应⽤场景了解么？" class="headerlink" title="Netty 应⽤场景了解么？"></a>Netty 应⽤场景了解么？</h3><ol>
<li><strong>作为 RPC （远程过程调用）框架的⽹络通信⼯具</strong> ： 我们在分布式系统中，<strong>不同服务节点之间经常 需要相互调⽤</strong>，这个时候就需要 RPC 框架了。<strong>不同服务节点的通信</strong>是如何做的 呢？可以使⽤ Netty 来做。⽐如我调⽤另外⼀个节点的⽅法的话，⾄少是要让对 ⽅知道我调⽤的是哪个类中的哪个⽅法以及相关参数吧！ </li>
<li><strong>实现⼀个⾃⼰的 HTTP 服务器</strong> ：通过 Netty 我们可以⾃⼰实现⼀个简单的 HTTP 服务器，这个⼤家应该不陌⽣。说到 HTTP 服务器的话，作为 Java 后端开发，我 们⼀般使⽤ Tomcat ⽐较多。⼀个最基本的 HTTP 服务器可要以处理常⻅的 HTTP Method 的请求，⽐如 POST 请求、GET 请求等等。</li>
<li><strong>实现⼀个即时通讯系统</strong> ： 使⽤ Netty 我们可以实现⼀个类似微信的即时聊天通讯系统</li>
<li><strong>实现消息推送系统</strong> ：市⾯上有很多消息推送系统都是基于 Netty 来做的。</li>
</ol>
<hr>
<h3 id="介绍⼀下-Netty-的核⼼组件？"><a href="#介绍⼀下-Netty-的核⼼组件？" class="headerlink" title="介绍⼀下 Netty 的核⼼组件？"></a>介绍⼀下 Netty 的核⼼组件？</h3><hr>
<h5 id="Bytebuf（字节容器）"><a href="#Bytebuf（字节容器）" class="headerlink" title="Bytebuf（字节容器）"></a>Bytebuf（字节容器）</h5><p><strong>⽹络通信最终都是通过字节流进⾏传输的。 ByteBuf 就是 Netty 提供的⼀个字节容器，其内部是⼀个字节数组。</strong></p>
<p>可以将 ByteBuf 看作是 Netty 对 Java NIO 提供了 ByteBuffer 字节容器的封装和抽象。</p>
<h6 id="为什么不直接使⽤-Java-NIO-提供的-ByteBuffer-呢？"><a href="#为什么不直接使⽤-Java-NIO-提供的-ByteBuffer-呢？" class="headerlink" title="为什么不直接使⽤ Java NIO 提供的 ByteBuffer 呢？"></a>为什么不直接使⽤ Java NIO 提供的 ByteBuffer 呢？</h6><p>因为 ByteBuffer 这个类使⽤起来过于复杂和繁琐。</p>
<hr>
<h5 id="Bootstrap-和-ServerBootstrap（启动引导类）"><a href="#Bootstrap-和-ServerBootstrap（启动引导类）" class="headerlink" title="Bootstrap 和 ServerBootstrap（启动引导类）"></a>Bootstrap 和 ServerBootstrap（启动引导类）</h5><p>Bootstrap 是客户端的启动引导类&#x2F;辅助类</p>
<p>ServerBootstrap 是服务端的启动引导类&#x2F;辅助类</p>
<ol>
<li>Bootstrap 通常使⽤ <strong>connect()</strong> ⽅法连接到远程的主机和端⼝，作为⼀ 个 Netty <strong>TCP</strong> 协议通信中的客户端。另外， Bootstrap 也可以通过 <strong>bind()</strong> ⽅法绑定本地的⼀个端⼝，作为 <strong>UDP</strong> 协议通信中的⼀端。</li>
<li>ServerBootstrap 通常使⽤ <strong>bind()</strong> ⽅法绑定本地的端⼝上，然后等待客户端的连接。 </li>
<li>Bootstrap 只需要配置⼀个线程组— EventLoopGroup ,⽽ ServerBootstrap 需要配置两个线程组— EventLoopGroup ，<strong>⼀个⽤于接收连接</strong>，<strong>⼀ 个⽤于具体的 IO 处理</strong>。</li>
</ol>
<hr>
<h5 id="Channel（⽹络操作抽象类）"><a href="#Channel（⽹络操作抽象类）" class="headerlink" title="Channel（⽹络操作抽象类）"></a>Channel（⽹络操作抽象类）</h5><p>通过 Channel 我们可以进⾏ I&#x2F;O 操 作。</p>
<p>⽐较常⽤的 Channel 接⼝实现类是 ：</p>
<ul>
<li>NioServerSocketChannel （服务端） </li>
<li>NioSocketChannel （客户端）</li>
</ul>
<hr>
<h5 id="x3D-x3D-EventLoop（事件循环）-x3D-x3D"><a href="#x3D-x3D-EventLoop（事件循环）-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;EventLoop（事件循环）&#x3D;&#x3D;"></a>&#x3D;&#x3D;EventLoop（事件循环）&#x3D;&#x3D;</h5><p>EventLoop 定义了 Netty 的核⼼抽象，⽤于处理连接的⽣命周期中所发⽣的事件。</p>
<p>EventLoop 的主要作⽤实际就是&#x3D;&#x3D;监听⽹络事件并调⽤事件处理器进⾏相关 I&#x2F;O 操作（读写）的处理&#x3D;&#x3D;。</p>
<hr>
<h5 id="Channel-和-EventLoop-的关系"><a href="#Channel-和-EventLoop-的关系" class="headerlink" title="Channel 和 EventLoop 的关系"></a>Channel 和 EventLoop 的关系</h5><p><strong>Channel 为 Netty ⽹络操作(读写等操作)抽象类， EventLoop 负责处理注册到 其上的 Channel 的 I&#x2F;O 操作，两者配合进⾏ I&#x2F;O 操作。</strong></p>
<hr>
<h5 id="EventloopGroup-和-EventLoop-的关系"><a href="#EventloopGroup-和-EventLoop-的关系" class="headerlink" title="EventloopGroup 和 EventLoop 的关系"></a>EventloopGroup 和 EventLoop 的关系</h5><p>EventLoopGroup 包含多个 EventLoop （每⼀个 EventLoop 通常内部包含 ⼀个线程），它管理着所有的 EventLoop 的⽣命周期</p>
<p><strong>EventLoop 处理的 I&#x2F;O 事件都将在它专有的 Thread 上被处理，即 Thr ead 和 EventLoop 属于 1 : 1 的关系，从⽽保证线程安全</strong></p>
<p><img src="https://img-blog.csdnimg.cn/20190328163409289.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxODUwNDQ5,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述"></p>
<hr>
<h5 id="ChannelHandler（消息处理器）-和-ChannelPipeline-（对象链表）"><a href="#ChannelHandler（消息处理器）-和-ChannelPipeline-（对象链表）" class="headerlink" title="ChannelHandler（消息处理器） 和 ChannelPipeline （对象链表）"></a>ChannelHandler（消息处理器） 和 ChannelPipeline （对象链表）</h5><p><strong>ChannelHandler 是消息的具体处理器，主要负责处理客户端&#x2F;服务端接收和发送的数据。</strong></p>
<p> ⼀ 个 Channel 包含⼀个 ChannelPipeline 。 ChannelPipeline 为 ChannelHandler 的链，⼀个 pipeline 上可以有多个 ChannelHandler 。</p>
<p>⼀个数据或者事件可能会被多个 Handler 处理 。当⼀个 ChannelHandler 处理完之后就将数据交给下⼀个 ChannelHandler 。</p>
<p>当 ChannelHandler 被添加到的 ChannelPipeline 它得到⼀个 ChannelHandlerContext ，它代表⼀个 ChannelHandler 和 ChannelPipeline 之 间的“绑定”。 ChannelPipeline 通过 ChannelHandlerContext 来间接管理 ChannelHandler 。</p>
<hr>
<h5 id="ChannelFuture（操作执⾏结果）"><a href="#ChannelFuture（操作执⾏结果）" class="headerlink" title="ChannelFuture（操作执⾏结果）"></a>ChannelFuture（操作执⾏结果）</h5><p>Netty 中所有的 I&#x2F;O 操作都为异步的，我们不能⽴刻得到操作是否执⾏成功</p>
<ol>
<li>addListener() ⽅法注册⼀个 ChannelFutureListener ，当操作执⾏成功或者失败时，监听就会⾃动触发返回结果</li>
<li>channel() ⽅法获取连接相关联的 Channel </li>
<li>sync() ⽅法让异步的操作编程同步的（等待异步操作执行完毕）</li>
</ol>
<hr>
<h5 id="NioEventLoopGroup-默认的构造函数会起多少线-程？"><a href="#NioEventLoopGroup-默认的构造函数会起多少线-程？" class="headerlink" title="NioEventLoopGroup 默认的构造函数会起多少线 程？"></a>NioEventLoopGroup 默认的构造函数会起多少线 程？</h5><p>默认的构造函数实际会起的线程数为 CPU 核⼼数*2 ，</p>
<p>每 个 NioEventLoopGroup 对象内部都会分配⼀组 NioEventLoop ，其⼤⼩是 nT hreads , 这样就构成了⼀个线程池， ⼀个 NIOEventLoop 和⼀个线程相对应</p>
<hr>
<h3 id="Reactor-线程模型"><a href="#Reactor-线程模型" class="headerlink" title="Reactor 线程模型"></a>Reactor 线程模型</h3><p>Reactor 是⼀种经典的线程模型，Reactor 模式<strong>基于事件驱动</strong>，采⽤多路复⽤将事件分发给相应的 Handler 处理，特别适合处理海量的 I&#x2F;O 事件。</p>
<p>在 Netty 主要靠 NioEventLoopGroup 线程池来实现具体的线程模型的 。</p>
<p>我们实现服务端的时候，⼀般会初始化两个线程组： </p>
<ol>
<li>bossGroup :接收连接。</li>
<li>workerGroup ：负责具体的处理，交由对应的 Handler 处理。</li>
</ol>
<h4 id="单线程-Reactor"><a href="#单线程-Reactor" class="headerlink" title="单线程 Reactor"></a>单线程 Reactor</h4><p><img src="https://img-blog.csdnimg.cn/20210309230152143.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDQ3MTQ5MA==,size_16,color_FFFFFF,t_70#pic_center" alt="img"></p>
<p>所有的 IO 操作都由同⼀个 NIO 线程处理。</p>
<p>单线程 Reactor 的优点是对系统资源消耗特别⼩，但是，没办法⽀撑⼤量请求的应⽤ 场景并且处理请求的时间可能⾮常慢。</p>
<h4 id="多线程-Reactor"><a href="#多线程-Reactor" class="headerlink" title="多线程 Reactor"></a>多线程 Reactor</h4><p><img src="https://img-blog.csdnimg.cn/20210309230210999.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDQ3MTQ5MA==,size_16,color_FFFFFF,t_70#pic_center" alt="img"></p>
<p>⼀个线程负责接受请求,⼀组 NIO 线程处理 IO 操作。</p>
<p>⼤部分场景下多线程 Reactor 模型是没有问题的，但是在⼀些并发连接数⽐较多（如 百万并发）的场景下，⼀个线程负责接受客户端请求就存在性能问题了。</p>
<h4 id="主从多线程-Reactor（子选择器）"><a href="#主从多线程-Reactor（子选择器）" class="headerlink" title="主从多线程 Reactor（子选择器）"></a>主从多线程 Reactor（子选择器）</h4><p><img src="https://img-blog.csdnimg.cn/20210309230233596.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDQ3MTQ5MA==,size_16,color_FFFFFF,t_70#pic_center" alt="img"></p>
<p>⼀组 NIO 线程负责接受请求，⼀组 NIO 线程处理 IO 操作。</p>
<hr>
<h4 id="什么是-TCP-粘包-x2F-拆包-有什么解决办法呢？"><a href="#什么是-TCP-粘包-x2F-拆包-有什么解决办法呢？" class="headerlink" title="什么是 TCP 粘包&#x2F;拆包?有什么解决办法呢？"></a>什么是 TCP 粘包&#x2F;拆包?有什么解决办法呢？</h4><h5 id="使⽤-Netty-⾃带的解码器"><a href="#使⽤-Netty-⾃带的解码器" class="headerlink" title="使⽤ Netty ⾃带的解码器"></a>使⽤ Netty ⾃带的解码器</h5><ul>
<li><strong>LineBasedFrameDecoder</strong> : 发送端发送数据包的时候，每个数据包之间以换行符作为分隔， LineBasedFrameDecoder 的⼯作原理是它依次遍历 ByteB uf 中的可读字节，判断是否有换⾏符，然后进⾏相应的截取。 </li>
<li><strong>DelimiterBasedFrameDecoder</strong> : 可以⾃定义分隔符解码器， LineBased FrameDecoder 实际上是⼀种特殊的 DelimiterBasedFrameDecoder 解码器。 </li>
<li><strong>FixedLengthFrameDecoder</strong> : 固定⻓度解码器，它能够按照指定的⻓度对消 息进⾏相应的拆包。如果不够指定的⻓度，则空格补全 </li>
<li><strong>LengthFieldBasedFrameDecoder</strong> ：基于⻓度字段的解码器，发送的数据 中有数据⻓度相关的信息。</li>
</ul>
<h5 id="自定义序列化解码器"><a href="#自定义序列化解码器" class="headerlink" title="自定义序列化解码器"></a>自定义序列化解码器</h5><ul>
<li>专⻔针对 Java 语⾔的：Kryo，FST 等等 </li>
<li>跨语⾔的：Protostuff（基于 protobuf 发展⽽来），ProtoBuf，Thrift，Avro， MsgPack 等等</li>
</ul>
<hr>
<h4 id="Netty-⻓连接、⼼跳机制了解么？"><a href="#Netty-⻓连接、⼼跳机制了解么？" class="headerlink" title="Netty ⻓连接、⼼跳机制了解么？"></a>Netty ⻓连接、⼼跳机制了解么？</h4><h5 id="心跳机制"><a href="#心跳机制" class="headerlink" title="心跳机制"></a>心跳机制</h5><p><strong>作用</strong>：在 TCP 保持⻓连接的过程中，没有交互的话，网络异常时客户端与服务器是⽆法发现对⽅已经掉线的。</p>
<p><strong>工作原理</strong></p>
<p>在 client 与 server 之间在⼀定时间内没有数据交互时, 即处于 &#x3D;&#x3D;idle&#x3D;&#x3D; 状态时, 客户端或服务器就会发送⼀个特殊的数据包给对⽅, 当接收⽅收到这个数 据报⽂后, 也⽴即发送⼀个<strong>特殊的数据报⽂</strong>, 回应发送⽅, 此即⼀个 <strong>PING-PONG 交 互</strong>。所以, 当某⼀端收到⼼跳消息后, 就知道了对⽅仍然在线, 这就确保 TCP 连接的有效性。</p>
<p>TCP 实际上⾃带的就有⻓连接选项和心跳包机制S0_KEEPALIVE。Netty实现心跳机制核心类<strong>IdleStateHandler</strong></p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230407183200324.png" alt="image-20230407183200324"></p>
<p><img src="https://s2.loli.net/2023/04/07/MPzUIqOEK3h7pVR.png"></p>
<hr>
<h4 id="x3D-x3D-Netty-的零拷⻉了解么？-x3D-x3D"><a href="#x3D-x3D-Netty-的零拷⻉了解么？-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;Netty 的零拷⻉了解么？&#x3D;&#x3D;"></a>&#x3D;&#x3D;Netty 的零拷⻉了解么？&#x3D;&#x3D;</h4><p><a target="_blank" rel="noopener" href="https://cloud.tencent.com/developer/article/1488088#:~:text=%20%E5%9C%A8%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%B1%82%E9%9D%A2%E4%B8%8A%E7%9A%84%E9%9B%B6%E6%8B%B7%E8%B4%9D%E6%98%AF%E6%8C%87%E9%81%BF%E5%85%8D%E5%9C%A8%E7%94%A8%E6%88%B7%E6%80%81%E4%B8%8E%E5%86%85%E6%A0%B8%E6%80%81%E4%B9%8B%E9%97%B4%E6%9D%A5%E5%9B%9E%E6%8B%B7%E8%B4%9D%E6%95%B0%E6%8D%AE%E7%9A%84%E6%8A%80%E6%9C%AF%E3%80%82Netty%E4%B8%AD%E7%9A%84%E9%9B%B6%E6%8B%B7%E8%B4%9D%E4%B8%8E%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%B1%82%E9%9D%A2%E4%B8%8A%E7%9A%84%E9%9B%B6%E6%8B%B7%E8%B4%9D%E4%B8%8D%E5%AE%8C%E5%85%A8%E4%B8%80%E6%A0%B7%2C,Netty%E7%9A%84%E9%9B%B6%E6%8B%B7%E8%B4%9D%E5%AE%8C%E5%85%A8%E6%98%AF%E5%9C%A8%E7%94%A8%E6%88%B7%E6%80%81%20%28Java%E5%B1%82%E9%9D%A2%29%E7%9A%84%EF%BC%8C%E6%9B%B4%E5%A4%9A%E6%98%AF%E6%95%B0%E6%8D%AE%E6%93%8D%E4%BD%9C%E7%9A%84%E4%BC%98%E5%8C%96%E3%80%82%20Netty%E7%9A%84%E6%8E%A5%E6%94%B6%E5%92%8C%E5%8F%91%E9%80%81ByteBuffer%E4%BD%BF%E7%94%A8%E7%9B%B4%E6%8E%A5%E5%86%85%E5%AD%98%E8%BF%9B%E8%A1%8CSocket%E8%AF%BB%E5%86%99%EF%BC%8C%E4%B8%8D%E9%9C%80%E8%A6%81%E8%BF%9B%E8%A1%8C%E5%AD%97%E8%8A%82%E7%BC%93%E5%86%B2%E5%8C%BA%E7%9A%84%E4%BA%8C%E6%AC%A1%E6%8B%B7%E8%B4%9D%E3%80%82">https://cloud.tencent.com/developer/article/1488088#:~:text=%20%E5%9C%A8%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%B1%82%E9%9D%A2%E4%B8%8A%E7%9A%84%E9%9B%B6%E6%8B%B7%E8%B4%9D%E6%98%AF%E6%8C%87%E9%81%BF%E5%85%8D%E5%9C%A8%E7%94%A8%E6%88%B7%E6%80%81%E4%B8%8E%E5%86%85%E6%A0%B8%E6%80%81%E4%B9%8B%E9%97%B4%E6%9D%A5%E5%9B%9E%E6%8B%B7%E8%B4%9D%E6%95%B0%E6%8D%AE%E7%9A%84%E6%8A%80%E6%9C%AF%E3%80%82Netty%E4%B8%AD%E7%9A%84%E9%9B%B6%E6%8B%B7%E8%B4%9D%E4%B8%8E%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%B1%82%E9%9D%A2%E4%B8%8A%E7%9A%84%E9%9B%B6%E6%8B%B7%E8%B4%9D%E4%B8%8D%E5%AE%8C%E5%85%A8%E4%B8%80%E6%A0%B7%2C,Netty%E7%9A%84%E9%9B%B6%E6%8B%B7%E8%B4%9D%E5%AE%8C%E5%85%A8%E6%98%AF%E5%9C%A8%E7%94%A8%E6%88%B7%E6%80%81%20%28Java%E5%B1%82%E9%9D%A2%29%E7%9A%84%EF%BC%8C%E6%9B%B4%E5%A4%9A%E6%98%AF%E6%95%B0%E6%8D%AE%E6%93%8D%E4%BD%9C%E7%9A%84%E4%BC%98%E5%8C%96%E3%80%82%20Netty%E7%9A%84%E6%8E%A5%E6%94%B6%E5%92%8C%E5%8F%91%E9%80%81ByteBuffer%E4%BD%BF%E7%94%A8%E7%9B%B4%E6%8E%A5%E5%86%85%E5%AD%98%E8%BF%9B%E8%A1%8CSocket%E8%AF%BB%E5%86%99%EF%BC%8C%E4%B8%8D%E9%9C%80%E8%A6%81%E8%BF%9B%E8%A1%8C%E5%AD%97%E8%8A%82%E7%BC%93%E5%86%B2%E5%8C%BA%E7%9A%84%E4%BA%8C%E6%AC%A1%E6%8B%B7%E8%B4%9D%E3%80%82</a></p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230407193955779.png" alt="image-20230407193955779"></p>
<p><strong>定义</strong></p>
<p>零复制（英语：Zero-copy；也译零拷⻉）技术是指计算机执⾏操作时，CPU 不需要先将数据从某处内存复制到另⼀个特定区域。这种技术通常⽤于通过⽹络传输⽂件以及I&#x2F;O操作时节省 CPU 周期和内存带宽。</p>
<ul>
<li>OS 层⾯上的 Zero-copy 通常指避免在 ⽤户态(User-space) 与 内核态(K ernel-space) 之间来回拷⻉数据。<img src="https://mmbiz.qpic.cn/mmbiz_png/OqTAl3WTC7EicibZpPpiaSHI238uMfVZibJLfzUefrlUS9Tv4o2JogzQWtDeMbFmTU5NuH4VCVqsuW54HSYRTDYNZg/640?wx_fmt=gif&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片"></li>
<li>Netty 层⾯ ，零拷⻉主要体现在用户态对于数据操作的优化。<img src="https://mmbiz.qpic.cn/mmbiz_png/OqTAl3WTC7Ejn0oXWsb4JZTOgOH40F5mDochiaylR5w9fiaFLY3TBcxibP36Xp4RKRyyXuYWlEgpuibpkItcUia4Eiag/640?wx_fmt=gif&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片"></li>
</ul>
<p><strong>Netty层面：</strong></p>
<ol>
<li>使⽤ Netty 提供的 CompositeByteBuf 类, 可以将多个 ByteBuf 合并为⼀ 个逻辑上的 ByteBuf , 避免了各个 ByteBuf 之间的拷⻉。<img src="https://ask.qcloudimg.com/http-save/yehe-5086501/ns0djumg0i.png?imageView2/2/w/2560/h/7000" alt="img"></li>
<li>ByteBuf ⽀持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同⼀个存储区 域的 ByteBuf , 避免了内存的拷⻉<img src="https://ask.qcloudimg.com/http-save/yehe-5086501/mcvawjt154.png?imageView2/2/w/2560/h/7000" alt="img"></li>
<li>通过 FileRegion 包装的 FileChannel.tranferTo 实现⽂件传输, 可以 直接将⽂件缓冲区的数据发送到⽬标 Channel , 避免了传统通过循环 write ⽅ 式导致的内存拷⻉问题.</li>
</ol>
<hr>
<h1 id="系统设计"><a href="#系统设计" class="headerlink" title="系统设计"></a>系统设计</h1><h2 id="基础-1"><a href="#基础-1" class="headerlink" title="基础"></a>基础</h2><hr>
<h2 id="安全"><a href="#安全" class="headerlink" title="安全"></a>安全</h2><h3 id="认证授权基础概念详解"><a href="#认证授权基础概念详解" class="headerlink" title="认证授权基础概念详解"></a>认证授权基础概念详解</h3><h4 id="认证-Authentication-和授权-Authorization-的区别是什么？"><a href="#认证-Authentication-和授权-Authorization-的区别是什么？" class="headerlink" title="认证 (Authentication) 和授权 (Authorization)的区别是什么？"></a>认证 (Authentication) 和授权 (Authorization)的区别是什么？</h4><ul>
<li><strong>认证 (Authentication)：</strong> 你是谁。</li>
<li><strong>授权 (Authorization)：</strong> 你有权限干什么。</li>
</ul>
<p>稍微正式点（啰嗦点）的说法就是 ：</p>
<ul>
<li><strong>Authentication（认证）</strong> 是验证您的身份的凭据（例如用户名&#x2F;用户 ID 和密码），通过这个凭据，系统得以知道你就是你，也就是说系统存在你这个用户。所以，Authentication 被称为身份&#x2F;用户验证。</li>
<li><strong>Authorization（授权）</strong> 发生在 <strong>Authentication（认证）</strong> 之后。授权嘛，光看意思大家应该就明白，它主要掌管我们访问系统的权限。比如有些特定资源只能具有特定权限的人才能访问比如 admin，有些对系统资源操作比如删除、添加、更新只能特定人才具有。</li>
</ul>
<hr>
<h4 id="RBAC-模型了解吗？"><a href="#RBAC-模型了解吗？" class="headerlink" title="RBAC 模型了解吗？"></a>RBAC 模型了解吗？</h4><h5 id="什么是-RBAC-呢？"><a href="#什么是-RBAC-呢？" class="headerlink" title="什么是 RBAC 呢？"></a><strong>什么是 RBAC 呢？</strong></h5><p>RBAC 即基于角色的权限访问控制（Role-Based Access Control）。这是一种通过角色关联权限，角色同时又关联用户的授权的方式。</p>
<hr>
<h5 id="x3D-x3D-如果没有-Cookie-的话-Session-还能用吗？-x3D-x3D"><a href="#x3D-x3D-如果没有-Cookie-的话-Session-还能用吗？-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;如果没有 Cookie 的话 Session 还能用吗？&#x3D;&#x3D;"></a>&#x3D;&#x3D;如果没有 Cookie 的话 Session 还能用吗？&#x3D;&#x3D;</h5><p>一般是通过 <code>Cookie</code> 来保存 <code>SessionID</code> ，假如你使用了 <code>Cookie</code> 保存 <code>SessionID</code> 的方案的话， 如果客户端禁用了 <code>Cookie</code>，那么 <code>Session</code> 就无法正常工作。</p>
<p>但是，并不是没有 <code>Cookie</code> 之后就不能用 <code>Session</code> 了，比如你可以将 <code>SessionID</code> 放在请求的 <code>url</code> 里面<code>https://javaguide.cn/?Session_id=xxx</code> 。这种方案的话可行，但是安全性和用户体验感降低。当然，为了你也可以对 <code>SessionID</code> 进行一次加密之后再传入后端。</p>
<hr>
<h5 id="什么是-JWT-JWT-由哪些部分组成？"><a href="#什么是-JWT-JWT-由哪些部分组成？" class="headerlink" title="什么是 JWT?JWT 由哪些部分组成？"></a>什么是 JWT?JWT 由哪些部分组成？</h5><hr>
<h5 id="如何基于-JWT-进行身份验证？-如何防止-JWT-被篡改？"><a href="#如何基于-JWT-进行身份验证？-如何防止-JWT-被篡改？" class="headerlink" title="如何基于 JWT 进行身份验证？ 如何防止 JWT 被篡改？"></a>如何基于 JWT 进行身份验证？ 如何防止 JWT 被篡改？</h5><hr>
<h5 id="什么是-SSO"><a href="#什么是-SSO" class="headerlink" title="什么是 SSO?"></a>什么是 SSO?</h5><p>SSO(Single Sign On)即单点登录说的是用户登陆多个子系统的其中一个就有权访问与其相关的其他系统。举个例子我们在登陆了京东金融之后，我们同时也成功登陆京东的京东超市、京东国际、京东生鲜等子系统。</p>
<hr>
<h5 id="什么是-OAuth-2-0？"><a href="#什么是-OAuth-2-0？" class="headerlink" title="什么是 OAuth 2.0？"></a>什么是 OAuth 2.0？</h5><p>实际上它就是一种授权机制，它的最终目的是为第三方应用颁发一个有时效性的令牌 Token，使得第三方应用能够通过该令牌获取相关的资源。</p>
<p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/system-design/security/20210615151716340.png" alt="img"></p>
<hr>
<h1 id="分布式（数据一致性）"><a href="#分布式（数据一致性）" class="headerlink" title="分布式（数据一致性）"></a>分布式（数据一致性）</h1><h2 id="分布式事务详解"><a href="#分布式事务详解" class="headerlink" title="分布式事务详解"></a>分布式事务详解</h2><h3 id="事务"><a href="#事务" class="headerlink" title="事务"></a>事务</h3><p><strong>事务是逻辑上的一组操作，要么都执行，要么都不执行。</strong></p>
<h3 id="数据库事务"><a href="#数据库事务" class="headerlink" title="数据库事务"></a>数据库事务</h3><p>对数据库的操作构成一个逻辑上的整体，这些操作遵循：<strong>要么全部执行成功，要么全部不执行。</strong></p>
<p><strong>只有保证了事务的持久性、原⼦性、隔离性之后，⼀ 致性才能得到保障。也就是说 A、I、&#x3D;&#x3D;D&#x3D;&#x3D; 是⼿段，C 是⽬的</strong></p>
<h4 id="x3D-x3D-数据事务的实现原理呢？-x3D-x3D"><a href="#x3D-x3D-数据事务的实现原理呢？-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;数据事务的实现原理呢？&#x3D;&#x3D;"></a>&#x3D;&#x3D;数据事务的实现原理呢？&#x3D;&#x3D;</h4><p>MySQL InnoDB 引擎使⽤ <strong>redo log(重做⽇志)</strong> 保证事务的<strong>持久性</strong>，使⽤ undo log(回滚⽇志) 来保证事务的<strong>原⼦性</strong>。MySQL InnoDB 引擎通过 <strong>锁机制、MVCC</strong> 等⼿段来保证事务的隔离性（ 默认⽀持的隔离级别是 <strong>REPEATABLE-READ</strong> ）。</p>
<h4 id="bin-log与redo-log有什么区别"><a href="#bin-log与redo-log有什么区别" class="headerlink" title="bin log与redo log有什么区别"></a>bin log与redo log有什么区别</h4><p><strong>相关数据库：</strong>bin log 会记录所有与数据库有关的⽇志记录，包括 InnoDB 、 MyISAM 等存储引擎的⽇志，⽽ redo log 只记 InnoDB 存储引擎的⽇志。</p>
<p><strong>记录的内容：</strong> bin log 记录的是关于⼀个事务的具体操作内容，即该⽇志是逻辑⽇志。⽽ redo log 记录的是关于每个页（ Page ）的更改的物理情况。</p>
<p><strong>写⼊的时间：</strong> bin log 仅在事务提交前进⾏提交，也就是只写磁盘⼀次。⽽在事务进⾏的过程中，却不断有 redo ertry 被写⼊ redo log 中。</p>
<p><strong>写⼊的⽅式：</strong> redo log 是循环写⼊和擦除， bin log 是追加写⼊，不会覆盖已经写的⽂件。</p>
<h3 id="分布式事务"><a href="#分布式事务" class="headerlink" title="分布式事务"></a>分布式事务</h3><h4 id="CAP理论和BASE理论"><a href="#CAP理论和BASE理论" class="headerlink" title="CAP理论和BASE理论"></a>CAP理论和BASE理论</h4><h5 id="一致性的3种级别"><a href="#一致性的3种级别" class="headerlink" title="一致性的3种级别"></a>一致性的3种级别</h5><ol>
<li>强一致性（任意时刻一致性）</li>
<li>弱一致性（某个时刻一致性）</li>
<li>最终一致性（一定时间一致性）</li>
</ol>
<hr>
<h5 id="柔性事务"><a href="#柔性事务" class="headerlink" title="柔性事务"></a>柔性事务</h5><p><strong>柔性事务追求最终一致性，是BASE理论+业务实践。例如：TCC、Saga、MQ事务、本地消息表</strong></p>
<hr>
<h5 id="刚性事务"><a href="#刚性事务" class="headerlink" title="刚性事务"></a>刚性事务</h5><p><strong>刚性事务追求强一致性，例如：2PC、3PC</strong></p>
<hr>
<h4 id="分布式事务解决方案"><a href="#分布式事务解决方案" class="headerlink" title="分布式事务解决方案"></a>分布式事务解决方案</h4><h5 id="2PC（两阶段提交协议–-gt-MySQL-redo-log）"><a href="#2PC（两阶段提交协议–-gt-MySQL-redo-log）" class="headerlink" title="2PC（两阶段提交协议–&gt;MySQL redo log）"></a>2PC（两阶段提交协议–&gt;MySQL redo log）</h5><ul>
<li>2 -&gt; 指代事务提交的 2 个阶段 </li>
<li>P-&gt; Prepare (准备阶段) </li>
<li>C -&gt;Commit（提交阶段）</li>
</ul>
<hr>
<h6 id="准备阶段"><a href="#准备阶段" class="headerlink" title="准备阶段"></a>准备阶段</h6><p>”询问“事务参与者执行本地数据库事务操作是否成功。</p>
<ol>
<li>事务协调者&#x2F;管理者 向所有参与者发送消息询问：“你是否可以执⾏事务操作 呢？”，并等待其答复。 </li>
<li>事务参与者 接收到消息之后，开始执⾏本地数据库事务预操作⽐如写 redo log&#x2F;undo log ⽇志。但是 ，此时并不会提交事务！ </li>
<li>事务参与者 如果执⾏本地数据库事务操作成功，那就回复：“就绪”，否则就 回复：“未就绪”。</li>
</ol>
<hr>
<h6 id="提交阶段"><a href="#提交阶段" class="headerlink" title="提交阶段"></a>提交阶段</h6><p>提交阶段的核⼼是“询问”事务参与者提交事务是否成功。</p>
<p> 当所有事务参与者都是“就绪”状态的话： </p>
<ol>
<li>事务协调者&#x2F;管理者 向所有参与者发送消息：“你们可以提交事务啦！” （commit 消息） </li>
<li>事务参与者 接收到 commit 消息 后执⾏ 提交本地数据库事务 操作，执⾏完 成之后 释放整个事务期间所占⽤的资源。</li>
<li>事务参与者 回复：“事务已经提交” （ack 消息）。</li>
<li>事务协调者&#x2F;管理者 收到所有 事务参与者 的 ack 消息 之后，整个分布式事 务过程正式结束</li>
</ol>
<p>当任⼀事务参与者是“未就绪”状态的话： </p>
<ol>
<li>事务协调者&#x2F;管理者 向所有参与者发送消息：“你们可以执⾏回滚操作了！” （rollback 消息）。 </li>
<li>事务参与者 接收到 rollback 消息 后执⾏ 本地数据库事务回滚 执⾏完成之 后 释放整个事务期间所占⽤的资源。 </li>
<li>事务参与者 回复：“事务已经回滚” （ack 消息）。 </li>
<li>事务协调者&#x2F;管理者 收到所有 事务参与者 的 ack 消息 之后，取消事务。</li>
</ol>
<p><strong>总结</strong></p>
<ol>
<li><p>准备阶段 的主要⽬的是测试 &#x3D;&#x3D;事务参与者 能否执⾏ 本地数据库事务 操作&#x3D;&#x3D;（!!! 注意：这⼀步并不会提交事务）。 </p>
</li>
<li><p>提交阶段 中 事务协调者&#x2F;管理者 会根据 准备阶段 中 事务参与者 的消息来决 定是&#x3D;&#x3D;执⾏事务提交还是回滚操作&#x3D;&#x3D;。 </p>
</li>
<li><p>提交阶段 之后⼀定会结束当前的分布式事务</p>
</li>
</ol>
<p><strong>2PC 的优点：</strong> </p>
<ul>
<li>实现起来⾮常简单，各⼤主流数据库⽐如 MySQL、Oracle 都有⾃⼰实现。</li>
<li>针对的是数据强⼀致性。不过，仍然可能存在数据不⼀致的情况。</li>
</ul>
<p>2PC 存在的问题：</p>
<ul>
<li>同步阻塞 ：事务参与者会在正式提交事务之前会⼀直占⽤相关的资源。⽐如 ⽤户⼩明转账给⼩红，那其他事务也要操作⽤户⼩明或⼩红的话，就会阻塞。 </li>
<li>数据不⼀致 ：由于⽹络问题或者事务协调者&#x2F;管理者宕机都有可能会造成数 据不⼀致的情况。⽐如在第2阶段（提交阶段），部分⽹络出现问题导致部 分参与者收不到 commit&#x2F;rollback 消息的话，就会导致数据不⼀致。 </li>
<li>单点问题 ： 事务协调者&#x2F;管理者在其中也是⼀个很重要的⻆⾊，如果事务协 调者&#x2F;管理者在准备(Prepare)阶段完成之后挂掉的话，事务参与者就会⼀直 卡在提交(Commit)阶段。</li>
</ul>
<h5 id="3PC（三阶段提交协议）"><a href="#3PC（三阶段提交协议）" class="headerlink" title="3PC（三阶段提交协议）"></a>3PC（三阶段提交协议）</h5><p>3PC 把 2PC 中的 准备阶段 (Prepare) 做了进⼀步细化，分为 2 个阶段：</p>
<ul>
<li><strong>询问阶段(CanCommit)</strong> ：这⼀步 不会执⾏事务操作，只会询问事务参与者 能否执⾏本地数据库事操作。 </li>
<li><strong>准备阶段(PreCommit)</strong> ：当所有事物参与者都返回“可执⾏”之后， 事务参 与者才会执⾏本地数据库事务预操作⽐如写 redo log&#x2F;undo log ⽇志。</li>
</ul>
<h5 id="TCC（补偿事务）"><a href="#TCC（补偿事务）" class="headerlink" title="TCC（补偿事务）"></a>TCC（补偿事务）</h5><h6 id="TCC事务机制简介"><a href="#TCC事务机制简介" class="headerlink" title="TCC事务机制简介"></a>TCC事务机制简介</h6><p><a target="_blank" rel="noopener" href="https://www.bytesoft.org/tcc-intro/">TCC事务机制简介 | 百特开源 (bytesoft.org)</a></p>
<ol>
<li><p><strong>Try（尝试）阶段</strong> : 尝试执⾏。完成业务检查，并预留好必需的业务资源。 </p>
</li>
<li><p><strong>Confirm（确认）阶段</strong> ：确认执⾏。当所有事务参与者的 Try 阶段执⾏成功 就会执⾏ Confirm ，Confirm 阶段会处理 Try 阶段预留的业务资源。否则， 就会执⾏ Cancel 。 </p>
</li>
<li><p><strong>Cancel（取消）阶段</strong> ：取消执⾏，释放 Try 阶段预留的业务资源。</p>
</li>
</ol>
<p><strong>TCC 模式不需要依赖于底层数据资源的事务⽀持，但是需要我们⼿动实现 更多的代码，属于 侵⼊业务代码 的⼀种分布式解决⽅案。</strong></p>
<hr>
<h6 id="关于如何实现一个TCC分布式事务框架的一点思考"><a href="#关于如何实现一个TCC分布式事务框架的一点思考" class="headerlink" title="关于如何实现一个TCC分布式事务框架的一点思考"></a>关于如何实现一个TCC分布式事务框架的一点思考</h6><p><a target="_blank" rel="noopener" href="https://www.bytesoft.org/how-to-impl-tcc/">关于如何实现一个TCC分布式事务框架的一点思考 | 百特开源 (bytesoft.org)</a></p>
<p><strong>TCC全局事务必须基于RM本地事务来实现全局事务</strong></p>
<p>基于RM本地事务实现TCC事务框架时，一个TCC型服务的cancel业务要么执行，要么不执行，不需要考虑部分执行的情况。</p>
<p><strong>TCC事务框架应该接管Spring容器的TransactionManager</strong></p>
<p>TCC服务的Try&#x2F;Confirm&#x2F;Cancel业务方法在RM上的数据存取操作，其RM本地事务是由Spring容器的PlatformTransactionManager来commit&#x2F;rollback的，TCC事务框架想要了解RM本地事务的状态，只能通过接管Spring的事务管理器功能。</p>
<p><strong>TCC事务框架应该具备故障恢复机制</strong></p>
<p><strong>TCC事务框架应该提供Confirm&#x2F;Cancel服务的幂等性保障</strong></p>
<p>服务的幂等性，是指针对同一个服务的多次(n&gt;1)请求和对它的单次(n&#x3D;1)请求，二者具有相同的副作用。</p>
<p>所有TCC服务的Confirm&#x2F;Cancel业务存在幂等性问题。TCC服务的公共问题应该由TCC事务框架来解决；而且，考虑一下由业务系统来负责幂等性需要考虑的问题，就会发现，这无疑增大了业务系统的复杂度。</p>
<p><strong>TCC事务框架不能盲目的依赖Cancel业务来回滚事务</strong></p>
<p>TCC事务通过Cancel业务来对Try业务进行回撤的机制暗含了一个事实：Try操作已经生效。也就是说，只有Try操作所参与的RM本地事务已经提交的情况下，才需要执行其Cancel操作进行回撤。没有执行、或者执行了但是其RM本地事务被rollback的Try业务，是一定不能执行其Cancel业务进行回撤的。</p>
<p><strong>Cancel业务与Try业务并行，甚至先于Try操作完成</strong></p>
<p><img src="https://s2.loli.net/2023/03/26/Bh3SlqiVzMKJoCn.png"></p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230326204712067.png" alt="image-20230326204712067"></p>
<p><strong>Cancel</strong></p>
<ul>
<li>将[B:Try]的本地事务标注为rollbackOnly，阻止其后续生效；</li>
<li>禁止其再次将事务上下文传递给其他远程分支，否则该问题将在其他分支上出现；</li>
<li>相应地，[B:Cancel]也不必执行，至少不能生效。</li>
</ul>
<p><strong>Confirm</strong></p>
<ul>
<li>Confirm业务在Try业务之后执行，若发现并行，则只能阻塞相应的Confirm业务操作；</li>
<li>在进入Confirm执行阶段之后，也不可以再提交同一全局事务内的新的Try操作的RM本地事务</li>
</ul>
<hr>
<p><strong>TCC服务复用性是不是相对较差？</strong></p>
<p><strong>TCC服务是否需要对外暴露三个服务接口？</strong></p>
<p>不需要。TCC服务与普通的服务一样，只需要暴露一个接口，也就是它的Try业务。</p>
<p><strong>TCC服务A的Confirm&#x2F;Cancel业务中能否调用它依赖的TCC服务B的Confirm&#x2F;Cancel业务？</strong></p>
<hr>
<h3 id="MQ事务"><a href="#MQ事务" class="headerlink" title="MQ事务"></a>MQ事务</h3><h4 id="简介-4"><a href="#简介-4" class="headerlink" title="简介"></a>简介</h4><p>以RocketMQ为例</p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230327105553928.png" alt="image-20230327105553928"></p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230327132042657.png" alt="image-20230327132042657"></p>
<ol>
<li>MQ 发送⽅（⽐如物流服务）在消息队列上开启⼀个事务，然后发送⼀个“半 消息”给 MQ Server&#x2F;Broker。事务提交之前，半消息对于 MQ 订阅⽅&#x2F;消费 者（⽐如第三⽅通知服务)不可⻅</li>
<li>“半消息”发送成功的话，MQ 发送⽅就开始执⾏本地事务。 </li>
<li>. MQ 发送⽅的本地事务执⾏成功的话，“半消息”变成正常消息，可以正常被 消费。MQ 发送⽅的本地事务执⾏失败的话，会直接回滚。</li>
</ol>
<h4 id="事务性消息发送过程"><a href="#事务性消息发送过程" class="headerlink" title="事务性消息发送过程"></a>事务性消息发送过程</h4><ol>
<li><p>生产者将半事务消息发送至 <code>RocketMQ Broker</code>。</p>
</li>
<li><p><code>RocketMQ Broker</code> 将消息持久化成功之后，向生产者返回 Ack 确认消息已经发送成功，此时消息暂不能投递，为半事务消息。</p>
</li>
<li><p>生产者开始执行本地事务逻辑。</p>
</li>
<li><p>生产者根据本地事务执行结果向服务端提交二次确认结果（Commit或是Rollback），服务端收到确认结果后处理逻辑如下：</p>
<ul>
<li><p>二次确认结果为Commit：服务端将半事务消息标记为可投递，并投递给消费者。</p>
</li>
<li><p>二次确认结果为Rollback：服务端将回滚事务，不会将半事务消息投递给消费者。</p>
</li>
</ul>
</li>
<li><p>在断网或者是生产者应用重启的特殊情况下，若服务端未收到发送者提交的二次确认结果，或服务端收到的二次确认结果为Unknown未知状态，经过固定时间后，服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。</p>
</li>
</ol>
<p>:::note 需要注意的是，服务端仅仅会按照参数尝试指定次数，超过次数后事务会强制回滚，因此未决事务的回查时效性非常关键，需要按照业务的实际风险来设置 :::</p>
<p>事务消息<strong>回查</strong>步骤如下： 7. 生产者收到消息回查后，需要检查对应消息的本地事务执行的最终结果。 8. 生产者根据检查得到的本地事务的最终状态再次提交二次确认，服务端仍按照步骤4对半事务消息进行处理。</p>
<h5 id="如果-MQ-发送⽅提交或者回滚事务消息时失败怎么办？"><a href="#如果-MQ-发送⽅提交或者回滚事务消息时失败怎么办？" class="headerlink" title="如果 MQ 发送⽅提交或者回滚事务消息时失败怎么办？"></a>如果 MQ 发送⽅提交或者回滚事务消息时失败怎么办？</h5><p>RocketMQ 中的 Broker 会定期去 MQ 发送⽅上反查这个事务的本地事务的执⾏ 情况，并根据反查结果决定提交或者回滚这个事务。</p>
<h5 id="如果正常消息没有被正确消费怎么办呢？"><a href="#如果正常消息没有被正确消费怎么办呢？" class="headerlink" title="如果正常消息没有被正确消费怎么办呢？"></a>如果正常消息没有被正确消费怎么办呢？</h5><p>消息消费失败的话，RocketMQ 会⾃动进⾏消费重试。如果超过最⼤重试次数这 个消息还是没有正确消费，RocketMQ 就会认为这个消息有问题，然后将其放到 <strong>死信队列</strong></p>
<hr>
<h2 id="CAP-amp-理论解释"><a href="#CAP-amp-理论解释" class="headerlink" title="CAP&amp; 理论解释"></a>CAP&amp; 理论解释</h2><h3 id="简介-5"><a href="#简介-5" class="headerlink" title="简介"></a>简介</h3><p>CAP 定理（CAP theorem）指出对于一个分布式系统来说，当设计读写操作时，只能同时满足以下三点中的两个：</p>
<ul>
<li><strong>一致性（Consistency）</strong> : 所有节点访问同一份最新的数据副本</li>
<li><strong>可用性（Availability）</strong>: 非故障的节点在合理的时间内返回合理的响应（不是错误或者超时的响应）。</li>
<li><strong>分区容错性（Partition Tolerance）</strong> : 分布式系统出现网络分区的时候，仍然能够对外提供服务。</li>
</ul>
<h3 id="什么是网络分区？"><a href="#什么是网络分区？" class="headerlink" title="什么是网络分区？"></a><strong>什么是网络分区？</strong></h3><p>分布式系统中，多个节点之前的网络本来是连通的，但是因为某些故障（比如部分节点网络出了问题）某些节点之间不连通了，整个网络就分成了几块区域，这就叫 <strong>网络分区</strong>。</p>
<p>&#x3D;&#x3D;网络分区之后，P是必须要满足的，在满足P的前提下，C和A二选一,否则正常情况下，CAP全部实现&#x3D;&#x3D;</p>
<hr>
<h3 id="BASE"><a href="#BASE" class="headerlink" title="BASE"></a>BASE</h3><h4 id="简介-6"><a href="#简介-6" class="headerlink" title="简介"></a>简介</h4><p><strong>BASE</strong> 是 <strong>Basically Available（基本可用）</strong> 、<strong>Soft-state（软状态）</strong> 和 <strong>Eventually Consistent（最终一致性）</strong></p>
<p>BASE 理论的核心思想</p>
<blockquote>
<p>牺牲数据的一致性来满足系统的高可用性，系统中一部分数据不可用或者不一致时，仍需要保持系统整体“主要可用”。</p>
</blockquote>
<p><strong>BASE 理论本质上是对 CAP 的延伸和补充，更具体地说，是对 CAP 中 AP 方案的一个补充。</strong></p>
<blockquote>
<p><strong>如果系统发生“分区”，我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话，我们要思考如何保证 CA 。</strong></p>
</blockquote>
<p>AP方案只是在系统分区的时候放弃一致性，而不是永远放弃一致性。在分区故障恢复后，系统应该达到最终一致性。</p>
<h4 id="BASE-理论三要素"><a href="#BASE-理论三要素" class="headerlink" title="BASE 理论三要素"></a>BASE 理论三要素</h4><h5 id="基本可用"><a href="#基本可用" class="headerlink" title="基本可用"></a>基本可用</h5><p><strong>什么叫允许损失部分可用性呢？</strong></p>
<ul>
<li><strong>响应时间上的损失</strong>: 正常情况下，处理用户请求需要 0.5s 返回结果，但是由于系统出现故障，处理用户请求的时间变为 3 s。</li>
<li><strong>系统功能上的损失</strong>：正常情况下，用户可以使用系统的全部功能，但是由于系统访问量突然剧增，系统的部分非核心功能无法使用。</li>
</ul>
<h5 id="软状态"><a href="#软状态" class="headerlink" title="软状态"></a>软状态</h5><p>软状态指允许系统中的数据存在中间状态（<strong>CAP 理论中的数据不一致</strong>），并认为该中间状态的存在不会影响系统的整体可用性，即<strong>允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。</strong></p>
<h5 id="最终一致性"><a href="#最终一致性" class="headerlink" title="最终一致性"></a>最终一致性</h5><p>最终一致性强调的是系统中所有的数据副本，在经过一段时间的同步后，最终能够达到一个一致的状态。</p>
<ul>
<li><strong>强一致性</strong> ：系统写入了什么，读出来的就是什么。</li>
<li><strong>弱一致性</strong> ：不一定可以读取到最新写入的值，也不保证多少时间之后读取到的数据是最新的，只是会尽量保证某个时刻达到数据一致的状态。</li>
<li><strong>最终一致性</strong> ：弱一致性的升级版，系统会保证在一定时间内达到数据一致的状态。</li>
</ul>
<h6 id="最终一致性实现方式"><a href="#最终一致性实现方式" class="headerlink" title="最终一致性实现方式"></a><strong>最终一致性实现方式</strong></h6><ul>
<li><strong>读时修复</strong> : 在读取数据时，检测数据的不一致，进行修复。比如 Cassandra 的 Read Repair 实现，具体来说，在向 Cassandra 系统查询数据的时候，如果检测到不同节点的副本数据不一致，系统就自动修复数据。</li>
<li><strong>写时修复</strong> : 在写入数据，检测数据的不一致时，进行修复。比如 Cassandra 的 Hinted Handoff 实现。具体来说，Cassandra 集群的节点之间远程写数据的时候，如果写失败 就将数据缓存下来，然后定时重传，修复数据的不一致性。</li>
<li><strong>异步修复</strong> : 这个是最常用的方式，通过定时对账检测副本数据的一致性，并修复。</li>
</ul>
<hr>
<h3 id="总结-9"><a href="#总结-9" class="headerlink" title="总结"></a>总结</h3><p><strong>ACID 是数据库事务完整性的理论，CAP 是分布式系统设计理论，BASE 是 CAP 理论中 AP 方案的延伸。</strong></p>
<hr>
<h2 id="Paxos-算法详细"><a href="#Paxos-算法详细" class="headerlink" title="Paxos 算法详细"></a>Paxos 算法详细</h2><h3 id="Basic-Paxos-算法"><a href="#Basic-Paxos-算法" class="headerlink" title="Basic Paxos 算法"></a>Basic Paxos 算法</h3><p>Basic Paxos 中存在 3 个重要的角色：</p>
<ol>
<li><strong>提议者（Proposer）</strong>：也可以叫做协调者（coordinator），提议者负责接受客户端的请求并发起提案。提案信息通常包括提案编号 (Proposal ID) 和提议的值 (Value)。</li>
<li><strong>接受者（Acceptor）</strong>：也可以叫做投票员（voter），负责对提议者的提案进行投票，同时需要记住自己的投票历史；</li>
<li><strong>学习者（Learner）</strong>：如果有超过半数接受者就某个提议达成了共识，那么学习者就需要接受这个提议，并就该提议作出运算，然后将运算结果返回给客户端。</li>
</ol>
<p><img src="https://oscimg.oschina.net/oscnet/up-890fa3212e8bf72886a595a34654918486c.png" alt="img"></p>
<hr>
<h2 id="Raft-算法详解"><a href="#Raft-算法详解" class="headerlink" title="Raft 算法详解"></a>Raft 算法详解</h2><hr>
<h2 id="API-网关详解"><a href="#API-网关详解" class="headerlink" title="API 网关详解"></a>API 网关详解</h2><h3 id="什么是网关？"><a href="#什么是网关？" class="headerlink" title="什么是网关？"></a>什么是网关？</h3><p>网关可以提供<strong>请求转发</strong>、<strong>请求过滤</strong>、安全认证（身份&#x2F;权限认证）、流量控制、负载均衡、降级熔断、日志、监控、参数校验、协议转换等功能。避免每个服务单独实现这些功能，提供一个全局的视图来统一管理这些功能。</p>
<hr>
<h2 id="分布式-ID-详解"><a href="#分布式-ID-详解" class="headerlink" title="分布式 ID 详解"></a>分布式 ID 详解</h2><h3 id="分布式-ID-介绍"><a href="#分布式-ID-介绍" class="headerlink" title="分布式 ID 介绍"></a>分布式 ID 介绍</h3><h4 id="什么是分布式-ID？"><a href="#什么是分布式-ID？" class="headerlink" title="什么是分布式 ID？"></a>什么是分布式 ID？</h4><p>在分库之后， 数据遍布在不同服务器上的数据库，数据库的自增主键已经没办法满足生成的主键唯一了。<strong>我们如何为不同的数据节点生成全局唯一主键呢？</strong></p>
<h4 id="分布式-ID-需要满足哪些要求"><a href="#分布式-ID-需要满足哪些要求" class="headerlink" title="分布式 ID 需要满足哪些要求?"></a>分布式 ID 需要满足哪些要求?</h4><p><strong>基础要求</strong></p>
<ul>
<li><strong>全局唯一</strong> ：ID 的全局唯一性肯定是首先要满足的！</li>
<li><strong>高性能</strong> ： 分布式 ID 的生成速度要快，对本地资源消耗要小。</li>
<li><strong>高可用</strong> ：生成分布式 ID 的服务要保证可用性无限接近于 100%。</li>
<li><strong>方便易用</strong> ：拿来即用，使用方便，快速接入！</li>
</ul>
<p><strong>额外要求</strong></p>
<ul>
<li><strong>安全</strong> ：ID 中不包含敏感信息。</li>
<li><strong>有序递增</strong> ：如果要把 ID 存放在数据库的话，ID 的有序性可以提升数据库写入速度。并且，很多时候 ，我们还很有可能会直接通过 ID 来进行排序。</li>
<li><strong>有具体的业务含义</strong> ：生成的 ID 如果能有具体的业务含义，可以让定位问题以及开发更透明化（通过 ID 就能确定是哪个业务）。</li>
<li><strong>独立部署</strong> ：也就是分布式系统单独有一个发号器服务，专门用来生成分布式 ID。这样就生成 ID 的服务可以和业务相关的服务解耦。不过，这样同样带来了网络调用消耗增加的问题。总的来说，如果需要用到分布式 ID 的场景比较多的话，独立部署的发号器服务还是很有必要的。</li>
</ul>
<hr>
<h3 id="分布式-ID-常见解决方案"><a href="#分布式-ID-常见解决方案" class="headerlink" title="分布式 ID 常见解决方案"></a>分布式 ID 常见解决方案</h3><h4 id="数据库-1"><a href="#数据库-1" class="headerlink" title="数据库"></a>数据库</h4><h5 id="数据库主键自增"><a href="#数据库主键自增" class="headerlink" title="数据库主键自增"></a>数据库主键自增</h5><hr>
<h5 id="数据库号段模式"><a href="#数据库号段模式" class="headerlink" title="数据库号段模式"></a>数据库号段模式</h5><p><strong>数据库号段模式的优缺点:</strong></p>
<p><code>current_max_id</code> 字段和<code>step</code>字段主要用于获取批量 ID，获取的批量 id 为： <code>current_max_id ~ current_max_id+step</code>。</p>
<ul>
<li><strong>优点</strong> ：ID 有序递增、存储消耗空间小</li>
<li><strong>缺点</strong> ：存在数据库单点问题（可以使用数据库集群解决，不过增加了复杂度）、ID 没有具体业务含义、安全问题（比如根据订单 ID 的递增规律就能推算出每天的订单量，商业机密啊！ ）</li>
</ul>
<hr>
<h5 id="NoSQL"><a href="#NoSQL" class="headerlink" title="NoSQL"></a>NoSQL</h5><p><strong>Redis 方案的优缺点：</strong></p>
<ul>
<li><strong>优点</strong> ： 性能不错并且生成的 ID 是有序递增的</li>
<li><strong>缺点</strong> ： 和数据库主键自增方案的缺点类似</li>
</ul>
<p><strong>MongoDB 方案的优缺点：</strong></p>
<ul>
<li><strong>优点</strong> ： 性能不错并且生成的 ID 是有序递增的</li>
<li><strong>缺点</strong> ： 需要解决重复 ID 问题（当机器时间不对的情况下，可能导致会产生重复 ID） 、有安全性问题（ID 生成有规律性）</li>
</ul>
<hr>
<h4 id="算法-1"><a href="#算法-1" class="headerlink" title="算法"></a>算法</h4><h5 id="UUID"><a href="#UUID" class="headerlink" title="UUID"></a>UUID</h5><p><strong>UUID 的优缺点</strong> （面试的时候可能会被问到的哦！） :</p>
<ul>
<li><strong>优点</strong> ：生成速度比较快、简单易用</li>
<li><strong>缺点</strong> ： 存储消耗空间大（32 个字符串，128 位） 、 不安全（基于 MAC 地址生成 UUID 的算法会造成 MAC 地址泄露)、无序（非自增）、没有具体业务含义、需要解决重复 ID 问题（当机器时间不对的情况下，可能导致会产生重复 ID）</li>
</ul>
<hr>
<h5 id="Snowflake-雪花算法"><a href="#Snowflake-雪花算法" class="headerlink" title="Snowflake(雪花算法)"></a>Snowflake(雪花算法)</h5><p> Snowflake 算法的优缺点 ：</p>
<ul>
<li><strong>优点</strong> ：生成速度比较快、生成的 ID 有序递增、比较灵活（可以对 Snowflake 算法进行简单的改造比如加入业务 ID）</li>
<li><strong>缺点</strong> ： 需要解决重复 ID 问题（依赖时间，当机器时间不对的情况下，可能导致会产生重复 ID）。</li>
</ul>
<h4 id="开源框架"><a href="#开源框架" class="headerlink" title="开源框架"></a>开源框架</h4><h5 id="UidGenerator-百度"><a href="#UidGenerator-百度" class="headerlink" title="UidGenerator(百度)"></a>UidGenerator(百度)</h5><h5 id="Leaf-美团"><a href="#Leaf-美团" class="headerlink" title="Leaf(美团)"></a>Leaf(美团)</h5><h5 id="Tinyid-滴滴"><a href="#Tinyid-滴滴" class="headerlink" title="Tinyid(滴滴)"></a>Tinyid(滴滴)</h5><hr>
<h2 id="分布式锁"><a href="#分布式锁" class="headerlink" title="分布式锁"></a>分布式锁</h2><h3 id="分布式锁介绍"><a href="#分布式锁介绍" class="headerlink" title="分布式锁介绍"></a>分布式锁介绍</h3><h4 id="本地锁"><a href="#本地锁" class="headerlink" title="本地锁"></a>本地锁</h4><p>通常使用ReetrantLock与synchronized关键字</p>
<p><img src="https://javaguide.cn/assets/jvm-local-lock.31b27bf9.png" alt="本地锁"></p>
<h4 id="分布式锁-1"><a href="#分布式锁-1" class="headerlink" title="分布式锁"></a>分布式锁</h4><p><img src="https://javaguide.cn/assets/distributed-lock.924e7216.png" alt="分布式锁"></p>
<p>一个最基本的分布式锁需要满足：</p>
<ul>
<li><strong>互斥</strong> ：任意一个时刻，锁只能被一个线程持有；</li>
<li><strong>高可用</strong> ：锁服务是高可用的。并且，即使客户端的释放锁的代码逻辑出现问题，锁最终一定还是会被释放，不会影响其他线程对共享资源的访问。</li>
<li><strong>可重入</strong>：一个节点获取了锁之后，还可以再次获取锁。</li>
</ul>
<hr>
<h5 id="基于-Redis-实现分布式锁"><a href="#基于-Redis-实现分布式锁" class="headerlink" title="基于 Redis 实现分布式锁"></a>基于 Redis 实现分布式锁</h5><h6 id="如何基于-Redis-实现一个最简易的分布式锁？"><a href="#如何基于-Redis-实现一个最简易的分布式锁？" class="headerlink" title="如何基于 Redis 实现一个最简易的分布式锁？"></a>如何基于 Redis 实现一个最简易的分布式锁？</h6><p>SETNX即 <strong>SET</strong> if <strong>N</strong>ot e<strong>X</strong>ists ，如果 key 不存在的话，才会设置 key 的值。如果 key 已经存在， <code>SETNX</code> 啥也不做。</p>
<p>选用 Lua 脚本判断key对应的value是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时，可以以原子性的方式执行，从而保证了锁释放操作的原子性。</p>
<p><img src="https://javaguide.cn/assets/distributed-lock-setnx.6f86bb0f.png" alt="Redis 实现简易分布式锁"></p>
<h6 id="为什么要给锁设置一个过期时间？"><a href="#为什么要给锁设置一个过期时间？" class="headerlink" title="为什么要给锁设置一个过期时间？"></a>为什么要给锁设置一个过期时间？</h6><p>为了避免锁无法被释放，<strong>给这个 key（也就是锁） 设置一个过期时间</strong> 。</p>
<p><strong>一定要保证设置指定 key 的值和过期时间是一个原子操作</strong>。</p>
<p>存在漏洞：<strong>如果操作共享资源的时间大于过期时间，就会出现锁提前过期的问题，进而导致分布式锁直接失效。如果锁的超时时间设置过长，又会影响到性能。</strong></p>
<p>解决方法：<strong>如果操作共享资源的操作还未完成，锁过期时间能够自己续期就好了！</strong></p>
<h6 id="如何实现锁的优雅续期？"><a href="#如何实现锁的优雅续期？" class="headerlink" title="如何实现锁的优雅续期？"></a>如何实现锁的优雅续期？</h6><p>Redisson中的分布式锁自带自动续期机制，提供了一个专门用来监控和续期锁的&#x3D;&#x3D;Watch Dog（看门狗）&#x3D;&#x3D;</p>
<p><img src="https://javaguide.cn/assets/distributed-lock-redisson-renew-expiration.878c6f2a.png" alt="Redisson 看门狗自动续期"></p>
<p><img src="https://img-blog.csdnimg.cn/20201119155758770.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM1ODQzMDk1,size_16,color_FFFFFF,t_70#pic_center" alt="在这里插入图片描述"></p>
<p>只有未指定锁超时时间，才会使用到 Watch Dog 自动续期机制。</p>
<h6 id="如何实现-x3D-x3D-可重入锁-x3D-x3D-？"><a href="#如何实现-x3D-x3D-可重入锁-x3D-x3D-？" class="headerlink" title="如何实现&#x3D;&#x3D;可重入锁&#x3D;&#x3D;？"></a>如何实现&#x3D;&#x3D;可重入锁&#x3D;&#x3D;？</h6><p>可重入锁指的是在一个线程中可以多次获取同一把锁</p>
<p>为每个锁关联一个可重入计数器和一个占有它的线程。当可重入计数器大于 0 时，则锁被占有，需要判断占有该锁的线程和请求获取锁的线程是否为同一个。</p>
<h6 id="Redis-如何解决集群情况下分布式锁的可靠性？"><a href="#Redis-如何解决集群情况下分布式锁的可靠性？" class="headerlink" title="Redis 如何解决集群情况下分布式锁的可靠性？"></a>Redis 如何解决集群情况下分布式锁的可靠性？</h6><p><img src="https://javaguide.cn/assets/redis-master-slave-distributed-lock.ccc5be73.png" alt="img"></p>
<p>Redlock 算法的思想是让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁，如果客户端能够和半数以上的实例成功地完成加锁操作，那么我们就认为，客户端成功地获得分布式锁，否则加锁失败。</p>
<h5 id="基于Zookeeper实现分布式锁"><a href="#基于Zookeeper实现分布式锁" class="headerlink" title="基于Zookeeper实现分布式锁"></a>基于Zookeeper实现分布式锁</h5><p>ZooKeeper 分布式锁是基于 <strong>临时顺序节点</strong> 和 <strong>Watcher（事件监听器）</strong> 实现的。</p>
<p>获取锁：</p>
<ol>
<li>首先我们要有一个持久节点<code>/locks</code>，客户端获取锁就是在<code>locks</code>下创建临时顺序节点。</li>
<li>假设客户端 1 创建了<code>/locks/lock1</code>节点，创建成功之后，会判断 <code>lock1</code>是否是 <code>/locks</code> 下最小的子节点。</li>
<li>如果 <code>lock1</code>是最小的子节点，则获取锁成功。否则，获取锁失败。</li>
<li>如果获取锁失败，则说明有其他的客户端已经成功获取锁。客户端 1 并不会不停地循环去尝试加锁，而是在前一个节点比如<code>/locks/lock0</code>上注册一个&#x3D;&#x3D;事件监听器&#x3D;&#x3D;。这个监听器的作用是当前一个节点释放锁之后通知客户端 1（避免无效自旋），这样客户端 1 就加锁成功了</li>
</ol>
<p>释放锁：</p>
<ol>
<li>成功获取锁的客户端在执行完业务流程之后，会将对应的子节点删除。</li>
<li>成功获取锁的客户端在出现故障之后，对应的子节点由于是临时顺序节点，也会被自动删除，避免了锁无法被释放。</li>
<li>我们前面说的事件监听器其实监听的就是这个&#x3D;&#x3D;子节点删除事件&#x3D;&#x3D;，子节点删除就意味着锁被释放。</li>
</ol>
<p><img src="https://javaguide.cn/assets/distributed-lock-zookeeper.f476fb11.png" alt="img"></p>
<h6 id="为什么要用临时顺序节点？"><a href="#为什么要用临时顺序节点？" class="headerlink" title="为什么要用临时顺序节点？"></a>为什么要用临时顺序节点？</h6><ul>
<li><strong>持久（PERSISTENT）节点</strong> ：一旦创建就一直存在即使 ZooKeeper 集群宕机，直到将其删除。</li>
<li><strong>临时（EPHEMERAL）节点</strong> ：临时节点的生命周期是与 <strong>客户端会话（session）</strong> 绑定的，<strong>会话消失则节点消失</strong> 。并且，<strong>临时节点只能做叶子节点</strong> ，不能创建子节点。（当客户端异常没来得及释放锁，会话生效节点自动删除）</li>
<li><strong>持久顺序（PERSISTENT_SEQUENTIAL）节点</strong> ：除了具有持久（PERSISTENT）节点的特性之外， 子节点的名称还具有顺序性。比如 <code>/node1/app0000000001</code> 、<code>/node1/app0000000002</code> 。</li>
<li><strong>临时顺序（EPHEMERAL_SEQUENTIAL）节点</strong> ：除了具备临时（EPHEMERAL）节点的特性之外，子节点的名称还具有顺序性。</li>
</ul>
<p>使用 Redis 实现分布式锁的时候，我们是通过过期时间来避免锁无法被释放导致死锁问题的，而 ZooKeeper 直接利用临时节点的特性即可。</p>
<hr>
<hr>
<h2 id="ZooKeeper"><a href="#ZooKeeper" class="headerlink" title="ZooKeeper"></a>ZooKeeper</h2><h3 id="ZooKeeper-相关概念总结-入门"><a href="#ZooKeeper-相关概念总结-入门" class="headerlink" title="ZooKeeper 相关概念总结(入门)"></a>ZooKeeper 相关概念总结(入门)</h3><h4 id="ZooKeeper-介绍"><a href="#ZooKeeper-介绍" class="headerlink" title="ZooKeeper 介绍"></a>ZooKeeper 介绍</h4><h5 id="ZooKeeper-概览"><a href="#ZooKeeper-概览" class="headerlink" title="ZooKeeper 概览"></a>ZooKeeper 概览</h5><p>ZooKeeper 是一个开源的<strong>分布式协调服务</strong>，它的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来，构成一个高效可靠的原语集，并以一系列简单易用的接口提供给用户使用。</p>
<blockquote>
<p><strong>原语：</strong> 操作系统或计算机网络用语范畴。是由若干条指令组成的，用于完成一定功能的一个过程。具有不可分割性·即原语的执行必须是连续的，在执行过程中不允许被中断。</p>
</blockquote>
<p><strong>ZooKeeper 为我们提供了高可用、高性能、稳定的分布式数据一致性解决方案，通常被用于实现诸如数据发布&#x2F;订阅、负载均衡、命名服务、分布式协调&#x2F;通知、集群管理、Master 选举、分布式锁和分布式队列等功能。</strong></p>
<p>另外，<strong>ZooKeeper 将数据保存在内存中，性能是非常棒的。 在“读”多于“写”的应用程序中尤其地高性能，因为“写”会导致所有的服务器间同步状态。（“读”多于“写”是协调服务的典型场景）。</strong></p>
<hr>
<h5 id="ZooKeeper-特点"><a href="#ZooKeeper-特点" class="headerlink" title="ZooKeeper 特点"></a>ZooKeeper 特点</h5><ul>
<li><strong>顺序一致性：</strong> 从同一客户端发起的事务请求，最终将会严格地按照顺序被应用到 ZooKeeper 中去。</li>
<li><strong>原子性：</strong> 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的，也就是说，要么整个集群中所有的机器都成功应用了某一个事务，要么都没有应用。</li>
<li><strong>单一系统映像 ：</strong> 无论客户端连到哪一个 ZooKeeper 服务器上，其看到的服务端数据模型都是一致的。</li>
<li><strong>可靠性：</strong> 一旦一次更改请求被应用，更改的结果就会被持久化，直到被下一次更改覆盖。</li>
</ul>
<hr>
<h5 id="ZooKeeper-典型应用场景"><a href="#ZooKeeper-典型应用场景" class="headerlink" title="ZooKeeper 典型应用场景"></a>ZooKeeper 典型应用场景</h5><ul>
<li><strong>分布式锁</strong> ： 通过创建唯一节点获得分布式锁，当获得锁的一方执行完相关代码或者是挂掉之后就释放锁。</li>
<li><strong>命名服务</strong> ：可以通过 ZooKeeper 的顺序节点生成全局唯一 ID</li>
<li><strong>数据发布&#x2F;订阅</strong> ：通过 <strong>Watcher 机制</strong> 可以很方便地实现数据发布&#x2F;订阅。当你将数据发布到 ZooKeeper 被监听的节点上，其他机器可通过监听 ZooKeeper 上节点的变化来实现配置的动态更新。</li>
</ul>
<hr>
<h4 id="x3D-x3D-ZooKeeper-重要概念解读-x3D-x3D"><a href="#x3D-x3D-ZooKeeper-重要概念解读-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;ZooKeeper 重要概念解读&#x3D;&#x3D;"></a>&#x3D;&#x3D;ZooKeeper 重要概念解读&#x3D;&#x3D;</h4><h5 id="Data-model（数据模型）"><a href="#Data-model（数据模型）" class="headerlink" title="Data model（数据模型）"></a>Data model（数据模型）</h5><p><strong>ZooKeeper 主要是用来协调服务的，而不是用来存储业务数据的，所以不要放比较大的数据在 znode 上，ZooKeeper 给出的上限是每个结点的数据大小最大是 1M。</strong></p>
<h5 id="znode（数据节点）"><a href="#znode（数据节点）" class="headerlink" title="znode（数据节点）"></a>znode（数据节点）</h5><p>介绍了 ZooKeeper 树形数据模型之后，我们知道每个数据节点在 ZooKeeper 中被称为 <strong>znode</strong>，它是 ZooKeeper 中数据的最小单元。你要存放的数据就放在上面，是你使用 ZooKeeper 过程中经常需要接触到的一个概念。</p>
<hr>
<h6 id="znode-4-种类型"><a href="#znode-4-种类型" class="headerlink" title="znode 4 种类型"></a>znode 4 种类型</h6><ul>
<li><strong>持久（PERSISTENT）节点</strong> ：一旦创建就一直存在即使 ZooKeeper 集群宕机，直到将其删除。</li>
<li><strong>临时（EPHEMERAL）节点</strong> ：临时节点的生命周期是与 <strong>客户端会话（session）</strong> 绑定的，<strong>会话消失则节点消失</strong> 。并且，<strong>临时节点只能做叶子节点</strong> ，不能创建子节点。</li>
<li><strong>持久顺序（PERSISTENT_SEQUENTIAL）节点</strong> ：除了具有持久（PERSISTENT）节点的特性之外， 子节点的名称还具有顺序性。比如 <code>/node1/app0000000001</code> 、<code>/node1/app0000000002</code> 。</li>
<li><strong>临时顺序（EPHEMERAL_SEQUENTIAL）节点</strong> ：除了具备临时（EPHEMERAL）节点的特性之外，子节点的名称还具有顺序性。</li>
</ul>
<hr>
<h6 id="znode-数据结构"><a href="#znode-数据结构" class="headerlink" title="znode 数据结构"></a>znode 数据结构</h6><p>每个 znode 由 2 部分组成:</p>
<ul>
<li><strong>stat</strong> ：状态信息</li>
<li><strong>data</strong> ： 节点存放的数据的具体内容</li>
</ul>
<hr>
<h5 id="Watcher（事件监听器）"><a href="#Watcher（事件监听器）" class="headerlink" title="Watcher（事件监听器）"></a>Watcher（事件监听器）</h5><p>Watcher（事件监听器），是 ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher，并且在一些特定事件触发的时候，ZooKeeper 服务端会将事件通知到感兴趣的客户端上去，该机制是 ZooKeeper 实现分布式协调服务的重要特性。</p>
<p><img src="https://oss.javaguide.cn/github/javaguide/distributed-system/zookeeper/zookeeper-watcher.png" alt="ZooKeeper Watcher 机制"></p>
<hr>
<h5 id="会话（Session）"><a href="#会话（Session）" class="headerlink" title="会话（Session）"></a>会话（Session）</h5><p>Session 可以看作是 ZooKeeper 服务器与客户端的之间的一个 TCP 长连接，通过这个连接，客户端能够通过心跳检测与服务器保持有效的会话，也能够向 ZooKeeper 服务器发送请求并接受响应，同时还能够通过该连接接收来自服务器的 Watcher 事件通知。</p>
<p>Session 有一个属性叫做：<code>sessionTimeout</code> ，<code>sessionTimeout</code> 代表会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时，只要在<code>sessionTimeout</code>规定的时间内能够重新连接上集群中任意一台服务器，那么之前创建的会话仍然有效。</p>
<p>另外，在为客户端创建会话之前，服务端首先会为每个客户端都分配一个 <code>sessionID</code>。由于 <code>sessionID</code>是 ZooKeeper 会话的一个重要标识，许多与会话相关的运行机制都是基于这个 <code>sessionID</code> 的，因此，无论是哪台服务器为客户端分配的 <code>sessionID</code>，都务必保证全局唯一。</p>
<hr>
<h5 id="ZooKeeper-集群"><a href="#ZooKeeper-集群" class="headerlink" title="ZooKeeper 集群"></a>ZooKeeper 集群</h5><p><img src="https://oss.javaguide.cn/github/javaguide/distributed-system/zookeeper/zookeeper-cluster.png" alt="ZooKeeper 集群架构"></p>
<p>组成 ZooKeeper 服务的服务器都会在内存中维护当前的服务器状态，并且每台服务器之间都互相保持着通信。集群间通过 ZAB 协议（ZooKeeper Atomic Broadcast）来保持&#x3D;&#x3D;数据的一致性&#x3D;&#x3D;。</p>
<p><strong>最典型集群模式： Master&#x2F;Slave 模式</strong>。在这种模式中，通常 Master 服务器作为主服务器提供写服务，其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。</p>
<hr>
<h6 id="ZooKeeper-集群角色"><a href="#ZooKeeper-集群角色" class="headerlink" title="ZooKeeper 集群角色"></a>ZooKeeper 集群角色</h6><p><img src="https://oss.javaguide.cn/github/javaguide/distributed-system/zookeeper/zookeeper-cluser-roles.png" alt="ZooKeeper 集群中角色"></p>
<p><img src="E:\XxdBlog\source_posts\images\test\image-20230330134810413.png" alt="image-20230330134810413"></p>
<p><img src="https://s2.loli.net/2023/03/30/nXPCF2jdA85gZfG.png"></p>
<hr>
<h6 id="ZooKeeper-集群-Leader-选举过程"><a href="#ZooKeeper-集群-Leader-选举过程" class="headerlink" title="ZooKeeper 集群 Leader 选举过程"></a>ZooKeeper 集群 Leader 选举过程</h6><ol>
<li><strong>Leader election（选举阶段）</strong>：节点在一开始都处于选举阶段，只要有一个节点得到超半数节点的票数，它就可以当选准 leader。</li>
<li><strong>Discovery（发现阶段）</strong> ：在这个阶段，followers 跟准 leader 进行通信，同步 followers 最近接收的事务提议。</li>
<li><strong>Synchronization（同步阶段）</strong> :同步阶段主要是利用 leader 前一阶段获得的最新提议历史，同步集群中所有的副本。同步完成之后准 leader 才会成为真正的 leader。</li>
<li><strong>Broadcast（广播阶段）</strong> :到了这个阶段，ZooKeeper 集群才能正式对外提供事务服务，并且 leader 可以进行消息广播。同时如果有新的节点加入，还需要对新节点进行同步。</li>
</ol>
<hr>
<h6 id="ZooKeeper-集群为啥最好-x3D-x3D-奇数-x3D-x3D-台？"><a href="#ZooKeeper-集群为啥最好-x3D-x3D-奇数-x3D-x3D-台？" class="headerlink" title="ZooKeeper 集群为啥最好&#x3D;&#x3D;奇数&#x3D;&#x3D;台？"></a>ZooKeeper 集群为啥最好&#x3D;&#x3D;奇数&#x3D;&#x3D;台？</h6><p>ZooKeeper 集群在宕掉几个 ZooKeeper 服务器之后，如果&#x3D;&#x3D;剩下的 ZooKeeper 服务器个数大于宕掉的个数的话整个 ZooKeeper 才依然可用&#x3D;&#x3D;。假如我们的集群中有 n 台 ZooKeeper 服务器，那么也就是剩下的服务数必须大于 n&#x2F;2。先说一下结论，2n 和 2n-1 的容忍度是一样的，都是 n-1，大家可以先自己仔细想一想，这应该是一个很简单的数学问题了。</p>
<p>比如假如我们有 3 台，那么最大允许宕掉 1 台 ZooKeeper 服务器，如果我们有 4 台的的时候也同样只允许宕掉 1 台。 假如我们有 5 台，那么最大允许宕掉 2 台 ZooKeeper 服务器，如果我们有 6 台的的时候也同样只允许宕掉 2 台。</p>
<hr>
<h6 id="ZooKeeper-选举的-x3D-x3D-过半机制-x3D-x3D-防止-x3D-x3D-脑裂-x3D-x3D"><a href="#ZooKeeper-选举的-x3D-x3D-过半机制-x3D-x3D-防止-x3D-x3D-脑裂-x3D-x3D" class="headerlink" title="ZooKeeper 选举的&#x3D;&#x3D;过半机制&#x3D;&#x3D;防止&#x3D;&#x3D;脑裂&#x3D;&#x3D;"></a>ZooKeeper 选举的&#x3D;&#x3D;过半机制&#x3D;&#x3D;防止&#x3D;&#x3D;脑裂&#x3D;&#x3D;</h6><p><strong>何为集群脑裂？</strong></p>
<p>对于一个集群，通常多台机器会部署在不同机房，来提高这个集群的可用性。保证可用性的同时，会发生一种机房间网络线路故障，导致机房间网络不通，而集群被割裂成几个小集群。这时候子集群各自选主导致“脑裂”的情况。</p>
<p>举例说明：比如现在有一个由 6 台服务器所组成的一个集群，部署在了 2 个机房，每个机房 3 台。正常情况下只有 1 个 leader，但是当两个机房中间网络断开的时候，每个机房的 3 台服务器都会认为另一个机房的 3 台服务器下线，而选出自己的 leader 并对外提供服务。若没有过半机制，当网络恢复的时候会发现有 2 个 leader。仿佛是 1 个大脑（leader）分散成了 2 个大脑，这就发生了脑裂现象。脑裂期间 2 个大脑都可能对外提供了服务，这将会带来数据一致性等问题。</p>
<p><strong>过半机制是如何防止脑裂现象产生的？</strong></p>
<p>ZooKeeper 的过半机制导致不可能产生 2 个 leader，因为少于等于一半是不可能产生 leader 的，这就使得不论机房的机器如何分配都不可能发生脑裂。</p>
<hr>
<h5 id="ZAB-协议和-Paxos-算法"><a href="#ZAB-协议和-Paxos-算法" class="headerlink" title="ZAB 协议和 Paxos 算法"></a>ZAB 协议和 Paxos 算法</h5><h6 id="ZAB-协议两种基本的模式：崩溃恢复和消息广播"><a href="#ZAB-协议两种基本的模式：崩溃恢复和消息广播" class="headerlink" title="ZAB 协议两种基本的模式：崩溃恢复和消息广播"></a>ZAB 协议两种基本的模式：崩溃恢复和消息广播</h6><p>ZAB 协议包括两种基本的模式，分别是</p>
<ul>
<li><strong>崩溃恢复</strong> ：当整个服务框架在启动过程中，或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时，ZAB 协议就会进入恢复模式并选举产生新的 Leader 服务器。当选举产生了新的 Leader 服务器，同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后，ZAB 协议就会退出恢复模式。其中，<strong>所谓的状态同步是指数据同步，用来保证集群中存在过半的机器能够和 Leader 服务器的数据状态保持一致</strong>。</li>
<li><strong>消息广播</strong> ：<strong>当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步，那么整个服务框架就可以进入消息广播模式了。</strong> 当一台同样遵守 ZAB 协议的服务器启动后加入到集群中时，如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播，那么新加入的服务器就会自觉地进入数据恢复模式：找到 Leader 所在的服务器，并与其进行数据同步，然后一起参与到消息广播流程中去。</li>
</ul>
<hr>
<h5 id="总结-10"><a href="#总结-10" class="headerlink" title="总结"></a>总结</h5><ol>
<li>ZooKeeper 本身就是一个分布式程序（只要半数以上节点存活，ZooKeeper 就能正常服务）。</li>
<li>为了保证高可用，最好是以集群形态来部署 ZooKeeper，这样只要集群中大部分机器是可用的（能够容忍一定的机器故障），那么 ZooKeeper 本身仍然是可用的。</li>
<li>ZooKeeper 将数据保存在内存中，这也就保证了 高吞吐量和低延迟（但是内存限制了能够存储的容量不太大，此限制也是保持 znode 中存储的数据量较小的进一步原因）。</li>
<li>ZooKeeper 是高性能的。 在“读”多于“写”的应用程序中尤其地明显，因为“写”会导致所有的服务器间同步状态。（“读”多于“写”是协调服务的典型场景。）</li>
<li>ZooKeeper 有临时节点的概念。 当创建临时节点的客户端会话一直保持活动，瞬时节点就一直存在。而当会话终结时，瞬时节点被删除。持久节点是指一旦这个 znode 被创建了，除非主动进行 znode 的移除操作，否则这个 znode 将一直保存在 ZooKeeper 上。</li>
<li>ZooKeeper 底层其实只提供了两个功能：① 管理（存储、读取）用户程序提交的数据；② 为用户程序提供数据节点监听服务。</li>
</ol>
<hr>
<h3 id="ZooKeeper-相关概念总结-进阶"><a href="#ZooKeeper-相关概念总结-进阶" class="headerlink" title="ZooKeeper 相关概念总结(进阶)"></a>ZooKeeper 相关概念总结(进阶)</h3><h4 id="基于Zookeeper的分布式系统一致性协议和算法"><a href="#基于Zookeeper的分布式系统一致性协议和算法" class="headerlink" title="基于Zookeeper的分布式系统一致性协议和算法"></a>基于Zookeeper的分布式系统一致性协议和算法</h4><h5 id="2PC"><a href="#2PC" class="headerlink" title="2PC"></a>2PC</h5><ul>
<li><strong>单点故障问题</strong>，如果协调者挂了那么整个系统都处于不可用的状态了。</li>
<li><strong>阻塞问题</strong>，即当协调者发送 <code>prepare</code> 请求，参与者收到之后如果能处理那么它将会进行事务的处理但并不提交，这个时候会一直占用着资源不释放，如果此时协调者挂了，那么这些资源都不会再释放了，这会极大影响性能。</li>
<li><strong>数据不一致问题</strong>，比如当第二阶段，协调者只发送了一部分的 <code>commit</code> 请求就挂了，那么也就意味着，收到消息的参与者会进行事务的提交，而后面没收到的则不会进行事务提交，那么这时候就会产生数据不一致性问题。</li>
</ul>
<hr>
<h5 id="3PC"><a href="#3PC" class="headerlink" title="3PC"></a>3PC</h5><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/80854635d48c42d896dbaa066abf5c26~tplv-k3u1fbpfcp-zoom-1.image" alt="3PC流程"></p>
<p><code>3PC</code> 在很多地方进行了超时中断的处理，比如协调者在指定时间内未收到全部的确认消息则进行事务中断的处理，这样能 <strong>减少同步阻塞的时间</strong> 。还有需要注意的是，**<code>3PC</code> 在 <code>DoCommit</code> 阶段参与者如未收到协调者发送的提交事务的请求，它会在一定时间内进行事务的提交<strong>。为什么这么做呢？是因为这个时候我们肯定</strong>保证了在第一阶段所有的协调者全部返回了可以执行事务的响应<strong>，这个时候我们有理由</strong>相信其他系统都能进行事务的执行和提交<strong>，所以</strong>不管**协调者有没有发消息给参与者，进入第三阶段参与者都会进行事务的提交操作。</p>
<p>&#x3D;&#x3D;关于超时中断&#x3D;&#x3D;</p>
<ul>
<li>在第一阶段，此时参与者存在等待协调者消息超时，返回abort</li>
<li>第二阶段<ul>
<li>协调者：一定时间内没有收到参与者的返回发送超时中断，向所有参与者发送abort，即参与者不进行redo与undo log的记录</li>
<li>参与者：在等待协调者请求过程中超时中断</li>
</ul>
</li>
<li>第三阶段：<ul>
<li>协调者：一定时间内没有收到参与者的返回发送超时中断，向所有参与者发送abort，即参与者进行回滚操作</li>
<li>参与者：当等待协调者请求超时时执行commit导致&#x3D;&#x3D;数据一致性&#x3D;&#x3D;问题</li>
</ul>
</li>
</ul>
<p>&#x3D;&#x3D;一致性没有得到解决&#x3D;&#x3D;</p>
<p>第三阶段：当网络出现延迟时，参与者在没有收到abort的情况下执行了事务提交，其他参与者执行了abort，此时出现了数据不一致。</p>
<hr>
<h5 id="Paxos-算法"><a href="#Paxos-算法" class="headerlink" title="Paxos 算法"></a><code>Paxos</code> 算法</h5><p><code>Paxos</code> 算法是基于<strong>消息传递且具有高度容错特性的一致性算法</strong>，是目前公认的解决分布式一致性问题最有效的算法之一，<strong>其解决的问题就是在分布式系统中如何就某个值（决议）达成一致</strong> 。</p>
<p>三个角色，分别为 <code>Proposer提案者</code>、<code>Acceptor表决者</code>、<code>Learner学习者</code>。</p>
<h6 id="prepare-阶段"><a href="#prepare-阶段" class="headerlink" title="prepare 阶段"></a>prepare 阶段</h6><ul>
<li><code>Proposer提案者</code>：负责提出 <code>proposal</code>，每个提案者在提出提案时都会首先获取到一个 <strong>具有全局唯一性的、递增的提案编号N</strong>，即在整个集群中是唯一的编号 N，然后将该编号赋予其要提出的提案，在<strong>第一阶段是只将提案编号发送给所有的表决者</strong>。</li>
<li><code>Acceptor表决者</code>：每个表决者在 <code>accept</code> 某提案后，会将该提案编号N记录在本地，这样每个表决者中保存的已经被 accept 的提案中会存在一个<strong>编号最大的提案</strong>，其编号假设为 <code>maxN</code>。每个表决者仅会 <code>accept</code> 编号大于自己本地 <code>maxN</code> 的提案，在批准提案时表决者会将以前接受过的最大编号的提案作为响应反馈给 <code>Proposer</code> 。</li>
</ul>
<hr>
<h6 id="accept-阶段"><a href="#accept-阶段" class="headerlink" title="accept 阶段"></a>accept 阶段</h6><p>当一个提案被 <code>Proposer</code> 提出后，如果 <code>Proposer</code> 收到了超过半数的 <code>Acceptor</code> 的批准（<code>Proposer</code> 本身同意），那么此时 <code>Proposer</code> 会给所有的 <code>Acceptor</code> 发送真正的提案（你可以理解为第一阶段为试探），这个时候 <code>Proposer</code> 就会发送提案的内容和提案编号。</p>
<p>表决者收到提案请求后会再次比较本身已经批准过的最大提案编号和该提案编号，如果该提案编号 <strong>大于等于</strong> 已经批准过的最大提案编号，那么就 <code>accept</code> 该提案（此时执行提案内容但不提交），随后将情况返回给 <code>Proposer</code> 。如果不满足则不回应或者返回 NO 。</p>
<p>当 <code>Proposer</code> 收到超过半数的 <code>accept</code> ，那么它这个时候会向所有的 <code>acceptor</code> 发送提案的提交请求。需要注意的是，因为上述仅仅是超过半数的 <code>acceptor</code> 批准执行了该提案内容，其他没有批准的并没有执行该提案内容，所以这个时候需要<strong>向未批准的 <code>acceptor</code> 发送提案内容和提案编号并让它无条件执行和提交</strong>，而对于前面已经批准过该提案的 <code>acceptor</code> 来说 <strong>仅仅需要发送该提案的编号</strong> ，让 <code>acceptor</code> 执行提交就行了。</p>
<p>而如果 <code>Proposer</code> 如果没有收到超过半数的 <code>accept</code> 那么它将会将 <strong>递增</strong> 该 <code>Proposal</code> 的编号，然后 <strong>重新进入 <code>Prepare</code> 阶段</strong> 。</p>
<hr>
<h6 id="paxos-算法的死循环问题"><a href="#paxos-算法的死循环问题" class="headerlink" title="paxos 算法的死循环问题"></a><code>paxos</code> 算法的死循环问题</h6><hr>
<h4 id="引出-ZAB"><a href="#引出-ZAB" class="headerlink" title="引出 ZAB"></a>引出 <code>ZAB</code></h4><h5 id="消息广播模式"><a href="#消息广播模式" class="headerlink" title="消息广播模式"></a>消息广播模式</h5><p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b64c7f25a5d24766889da14260005e31~tplv-k3u1fbpfcp-zoom-1.image" alt="消息广播"></p>
<p>Deque保证**<code>ZAB</code> 让 <code>Follower</code> 和 <code>Observer</code> 保证顺序性** 。</p>
<p> <strong>通过 <code>TCP</code></strong> 来进行网络通信的，保证了消息的发送顺序性，接受顺序性也得到了保证。</p>
<h5 id="崩溃恢复模式"><a href="#崩溃恢复模式" class="headerlink" title="崩溃恢复模式"></a>崩溃恢复模式</h5><h6 id="选举过程"><a href="#选举过程" class="headerlink" title="选举过程"></a>选举过程</h6><p><code>Leader</code> 选举可以分为两个不同的阶段，</p>
<ul>
<li>第一个是我们提到的 <code>Leader</code> 宕机需要重新选举，</li>
<li>第二则是当 <code>Zookeeper</code> 启动时需要进行系统的 <code>Leader</code> 初始化选举。</li>
</ul>
<p>下面我先来介绍一下 <code>ZAB</code> 是如何进行初始化选举的。</p>
<p>假设我们集群中有3台机器，那也就意味着我们需要两台以上同意（超过半数）。比如这个时候我们启动了 <code>server1</code> ，它会首先 <strong>投票给自己</strong> ，投票内容为服务器的 <code>myid</code> 和 <code>ZXID</code> ，因为初始化所以 <code>ZXID</code> 都为0，此时 <code>server1</code> 发出的投票为 (1,0)。但此时 <code>server1</code> 的投票仅为1，所以不能作为 <code>Leader</code> ，此时还在选举阶段所以整个集群处于 <strong><code>Looking</code> 状态</strong>。</p>
<p>接着 <code>server2</code> 启动了，它首先也会将投票选给自己(2,0)，并将投票信息广播出去（<code>server1</code>也会，只是它那时没有其他的服务器了），<code>server1</code> 在收到 <code>server2</code> 的投票信息后会将投票信息与自己的作比较。**首先它会比较 <code>ZXID</code> ，<code>ZXID</code> 大的优先为 <code>Leader</code>，如果相同则比较 <code>myid</code>，<code>myid</code> 大的优先作为 <code>Leader</code>**。所以此时<code>server1</code> 发现 <code>server2</code> 更适合做 <code>Leader</code>，它就会将自己的投票信息更改为(2,0)然后再广播出去，之后<code>server2</code> 收到之后发现和自己的一样无需做更改，并且自己的 <strong>投票已经超过半数</strong> ，则 **确定 <code>server2</code> 为 <code>Leader</code>**，<code>server1</code> 也会将自己服务器设置为 <code>Following</code> 变为 <code>Follower</code>。整个服务器就从 <code>Looking</code> 变为了正常状态。</p>
<p>当 <code>server3</code> 启动发现集群没有处于 <code>Looking</code> 状态时，它会直接以 <code>Follower</code> 的身份加入集群</p>
<p><strong>确保已经被Leader提交的提案最终能够被所有的Follower提交</strong> </p>
<p><strong>跳过那些已经被丢弃的提案</strong> 。</p>
<hr>
<h3 id="Zookeeper的几个经典应用场景"><a href="#Zookeeper的几个经典应用场景" class="headerlink" title="Zookeeper的几个经典应用场景"></a>Zookeeper的几个经典应用场景</h3><h4 id="选主"><a href="#选主" class="headerlink" title="选主"></a>选主</h4><h4 id="分布式锁-2"><a href="#分布式锁-2" class="headerlink" title="分布式锁"></a>分布式锁</h4><h4 id="命名服务"><a href="#命名服务" class="headerlink" title="命名服务"></a>命名服务</h4><h4 id="集群管理与注册中心"><a href="#集群管理与注册中心" class="headerlink" title="集群管理与注册中心"></a>集群管理与注册中心</h4><hr>
<h1 id="高性能"><a href="#高性能" class="headerlink" title="高性能"></a>高性能</h1><h2 id="读写分离和分库分表详解"><a href="#读写分离和分库分表详解" class="headerlink" title="读写分离和分库分表详解"></a>读写分离和分库分表详解</h2><h3 id="读写分离"><a href="#读写分离" class="headerlink" title="读写分离"></a>读写分离</h3><h4 id="主从复制原理是什么？"><a href="#主从复制原理是什么？" class="headerlink" title="主从复制原理是什么？"></a>主从复制原理是什么？</h4><p><img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/78816271d3ab52424bfd5ad3086c1a0f.png" alt="MySQL主从复制"></p>
<ul>
<li>主库将数据库中数据的变化写入到 binlog</li>
<li>从库连接主库</li>
<li>从库会创建一个 I&#x2F;O 线程向主库请求更新的 binlog</li>
<li>主库会创建一个 binlog dump 线程来发送 binlog ，从库中的 I&#x2F;O 线程负责接收</li>
<li>从库的 I&#x2F;O 线程将接收的 binlog 写入到 relay log 中。</li>
<li>从库的 SQL 线程读取 relay log 同步数据本地（也就是再执行一遍 SQL ）。</li>
</ul>
<p>🌕 简单总结一下：</p>
<p><strong>MySQL 主从复制是依赖于 binlog 。另外，常见的一些同步 MySQL 数据到其他数据源的工具（比如 canal）的底层一般也是依赖 binlog 。</strong></p>
<hr>
<h3 id="分库分表"><a href="#分库分表" class="headerlink" title="分库分表"></a>分库分表</h3><hr>
<h3 id="总结-11"><a href="#总结-11" class="headerlink" title="总结"></a>总结</h3><ul>
<li>读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。 这样的话，就能够小幅提升写性能，大幅提升读性能。</li>
<li>读写分离基于主从复制，MySQL 主从复制是依赖于 binlog 。</li>
<li><strong>分库</strong> 就是将数据库中的数据分散到不同的数据库上。<strong>分表</strong> 就是对单表的数据进行拆分，可以是垂直拆分，也可以是水平拆分。</li>
<li>引入分库分表之后，需要系统解决事务、分布式 id、无法 join 操作问题。</li>
<li>ShardingSphere 绝对可以说是当前分库分表的首选！ShardingSphere 的功能完善，除了支持读写分离和分库分表，还提供分布式事务、数据库治理等功能。另外，ShardingSphere 的生态体系完善，社区活跃，文档完善，更新和发布比较频繁。</li>
</ul>
<hr>
<h2 id="x3D-x3D-消息队列-x3D-x3D"><a href="#x3D-x3D-消息队列-x3D-x3D" class="headerlink" title="&#x3D;&#x3D;消息队列&#x3D;&#x3D;"></a>&#x3D;&#x3D;消息队列&#x3D;&#x3D;</h2><p><strong>中间件就是一类为应用软件服务的软件，应用软件是为用户服务的，用户不会接触或者使用到中间件。</strong></p>
<h3 id="消息队列基础知识总结"><a href="#消息队列基础知识总结" class="headerlink" title="消息队列基础知识总结"></a>消息队列基础知识总结</h3><p><img src="https://s2.loli.net/2023/04/10/pAoiQPOj4SE2Mev.png"></p>
<h4 id="消息队列有什么用？"><a href="#消息队列有什么用？" class="headerlink" title="消息队列有什么用？"></a>消息队列有什么用？</h4><p>通常来说，使用消息队列能为我们的系统带来下面三点好处：</p>
<ol>
<li><strong>通过异步处理提高系统性能（减少响应所需时间）</strong></li>
<li><strong>削峰&#x2F;限流</strong></li>
<li><strong>降低系统耦合性。</strong></li>
</ol>
<h4 id="通过异步处理提高系统性能（减少响应所需时间）"><a href="#通过异步处理提高系统性能（减少响应所需时间）" class="headerlink" title="通过异步处理提高系统性能（减少响应所需时间）"></a>通过异步处理提高系统性能（减少响应所需时间）</h4><p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/Asynchronous-message-queue.png" alt="通过异步处理提高系统性能"></p>
<p>将用户的请求数据存储到消息队列之后就立即返回结果。随后，系统再对消息进行消费。</p>
<p>因为用户请求数据写入消息队列之后就立即返回给用户了，但是请求数据在后续的业务校验、写数据库等操作中可能失败。因此，<strong>使用消息队列进行异步处理之后，需要适当修改业务流程进行配合</strong>，比如用户在提交订单之后，订单数据写入消息队列，不能立即返回用户订单提交成功，需要在消息队列的订单消费者进程真正处理完该订单之后，甚至出库后，再通过电子邮件或短信通知用户订单成功，以免交易纠纷。这就类似我们平时手机订火车票和电影票。</p>
<hr>
<h4 id="削峰-x2F-限流"><a href="#削峰-x2F-限流" class="headerlink" title="削峰&#x2F;限流"></a>削峰&#x2F;限流</h4><p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/%E5%89%8A%E5%B3%B0-%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97.png" alt="削峰"></p>
<h4 id="降低系统耦合性"><a href="#降低系统耦合性" class="headerlink" title="降低系统耦合性"></a>降低系统耦合性</h4><p>使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用，那么新增模块或者修改模块就对其他模块影响较小，这样系统的可扩展性无疑更好一些。</p>
<p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97-%E8%A7%A3%E8%80%A6.png" alt="解耦"></p>
<p>生产者（客户端）发送消息到消息队列中去，接受者（服务端）处理消息，需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合，这显然也提高了系统的扩展性。</p>
<p><strong>消息队列使用发布-订阅模式工作，消息发送者（生产者）发布消息，一个或多个消息接受者（消费者）订阅消息。</strong> 从上图可以看到<strong>消息发送者（生产者）和消息接受者（消费者）之间没有直接耦合</strong>，消息发送者将消息发送至分布式消息队列即结束对消息的处理，消息接受者从分布式消息队列获取该消息后进行后续处理，并不需要知道该消息从何而来。<strong>对新增业务，只要对该类消息感兴趣，即可订阅该消息，对原有系统和业务没有任何影响，从而实现网站业务的可扩展性设计</strong>。<img src="https://javaguide.cn/assets/message-queue-pub-sub-model.63a717b4.png" alt="发布/订阅（Pub/Sub）模型"></p>
<p>另外，为了避免消息队列服务器宕机造成消息丢失，会将成功发送到消息队列的消息存储在消息生产者服务器上，等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后，生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。</p>
<hr>
<h4 id="MQ设计模式"><a href="#MQ设计模式" class="headerlink" title="MQ设计模式"></a>MQ设计模式</h4><h5 id="简单模式"><a href="#简单模式" class="headerlink" title="简单模式"></a>简单模式</h5><h5 id="工作模式"><a href="#工作模式" class="headerlink" title="工作模式"></a>工作模式</h5><h5 id="消息发布和订阅"><a href="#消息发布和订阅" class="headerlink" title="消息发布和订阅"></a>消息发布和订阅</h5><h5 id="路由模式"><a href="#路由模式" class="headerlink" title="路由模式"></a>路由模式</h5><h6 id="主题模式"><a href="#主题模式" class="headerlink" title="主题模式"></a>主题模式</h6><p><img src="E:\XxdBlog\source_posts\images\test\image-20230402195154372.png" alt="image-20230402195154372"></p>
<p><img src="https://s2.loli.net/2023/04/02/UyIczG7FtWlea9k.png"></p>
<hr>
<h4 id="使用消息队列会带来哪些问题？"><a href="#使用消息队列会带来哪些问题？" class="headerlink" title="使用消息队列会带来哪些问题？"></a>使用消息队列会带来哪些问题？</h4><ul>
<li><strong>系统可用性降低：</strong> 系统可用性在某种程度上降低，为什么这样说呢？在加入 MQ 之前，你不用考虑消息丢失或者说 MQ 挂掉等等的情况，但是，引入 MQ 之后你就需要去考虑了！</li>
<li><strong>系统复杂性提高：</strong> 加入 MQ 之后，你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题！</li>
<li><strong>一致性问题：</strong> 我上面讲了消息队列可以实现异步，消息队列带来的异步确实可以提高系统响应速度。但是，万一消息的真正消费者并没有正确消费消息怎么办？这样就会导致数据不一致的情况了!</li>
</ul>
<hr>
<h4 id="JMS-和-AMQP"><a href="#JMS-和-AMQP" class="headerlink" title="JMS 和 AMQP"></a>JMS 和 AMQP</h4><h4 id="JMS-是什么？"><a href="#JMS-是什么？" class="headerlink" title="JMS 是什么？"></a>JMS 是什么？</h4><p><strong>JMS（JAVA Message Service，Java 消息服务）API 是一个消息服务的标准或者说是规范</strong>，允许应用程序组件基于 JavaEE 平台创建、发送、接收和读取消息。它使分布式通信耦合度更低，消息服务更加可靠以及异步性。</p>
<h4 id="JMS-两种消息模型"><a href="#JMS-两种消息模型" class="headerlink" title="JMS 两种消息模型"></a>JMS 两种消息模型</h4><h5 id="点到点（P2P）模型"><a href="#点到点（P2P）模型" class="headerlink" title="点到点（P2P）模型"></a>点到点（P2P）模型</h5><p><img src="https://javaguide.cn/assets/message-queue-queue-model.3aa809bf.png" alt="队列模型"></p>
<p>**队列（Queue）*<em>作为消息通信载体；满足*<em>生产者与消费者模式</em></em></p>
<h5 id="发布-x2F-订阅（Pub-x2F-Sub）模型"><a href="#发布-x2F-订阅（Pub-x2F-Sub）模型" class="headerlink" title="发布&#x2F;订阅（Pub&#x2F;Sub）模型"></a>发布&#x2F;订阅（Pub&#x2F;Sub）模型</h5><p><img src="https://javaguide.cn/assets/message-queue-pub-sub-model.63a717b4.png" alt="发布/订阅（Pub/Sub）模型"></p>
<p>发布订阅模型（Pub&#x2F;Sub） 使用<strong>主题（Topic）**作为消息通信载体，类似于**广播模式**；发布者发布一条消息，该消息通过主题传递给所有的订阅者，</strong>在一条消息广播之后才订阅的用户则是收不到该条消息的**。</p>
<hr>
<h4 id="AMQP-是什么？"><a href="#AMQP-是什么？" class="headerlink" title="AMQP 是什么？"></a>AMQP 是什么？</h4><p>AMQP，即 Advanced Message Queuing Protocol，一个提供统一消息服务的应用层标准 <strong>高级消息队列协议</strong>（二进制应用层协议），是应用层协议的一个开放标准，为面向消息的中间件设计，兼容 JMS。基于此协议的客户端与消息中间件可传递消息，并不受客户端&#x2F;中间件同产品，不同的开发语言等条件的限制。</p>
<p><strong>RabbitMQ 就是基于 AMQP 协议实现的。</strong></p>
<h4 id="JMS-vs-AMQP"><a href="#JMS-vs-AMQP" class="headerlink" title="JMS vs AMQP"></a>JMS vs AMQP</h4><p><img src="E:\XxdBlog\source_posts\images\test\image-20230311155545137.png" alt="image-20230311155545137"></p>
<h4 id="总结-12"><a href="#总结-12" class="headerlink" title="总结"></a><strong>总结</strong></h4><ul>
<li>AMQP 为消息定义了线路层（wire-level protocol）的协议，而 JMS 所定义的是 API 规范。在 Java 体系中，多个 client 均可以通过 JMS 进行交互，不需要应用修改代码，但是其对跨平台的支持较差。而 AMQP 天然具有跨平台、跨语言特性。</li>
<li>JMS 支持 <code>TextMessage</code>、<code>MapMessage</code> 等复杂的消息类型；而 AMQP 仅支持 <code>byte[]</code> 消息类型（复杂的类型可序列化后发送）。</li>
<li>由于 Exchange 提供的路由算法，AMQP 可以提供多样化的路由方式来传递消息到消息队列，而 JMS 仅支持 队列 和 主题&#x2F;订阅 方式两种。</li>
</ul>
<hr>
<h4 id="RPC-和消息队列的区别"><a href="#RPC-和消息队列的区别" class="headerlink" title="RPC 和消息队列的区别"></a>RPC 和消息队列的区别</h4><p><img src="E:\XxdBlog\source_posts\images\test\image-20230408140230927.png" alt="image-20230408140230927"><img src="https://s2.loli.net/2023/04/08/qRGUA26MFovcytE.png"></p>
<p>RPC 和消息队列都是分布式微服务系统中重要的组件之一，下面我们来简单对比一下两者：</p>
<ul>
<li><strong>从用途来看</strong> ：RPC 主要用来解决两个服务的远程通信问题，不需要了解底层网络的通信机制。通过 RPC 可以帮助我们调用远程计算机上某个服务的方法，这个过程就像调用本地方法一样简单。消息队列主要用来降低系统耦合性、实现任务异步、有效地进行流量削峰。</li>
<li><strong>从通信方式来看</strong> ：RPC 是双向直接网络通讯，消息队列是单向引入中间载体的网络通讯。</li>
<li><strong>从架构上来看</strong> ：消息队列需要把消息存储起来，RPC 则没有这个要求，因为前面也说了 RPC 是双向直接网络通讯。</li>
<li><strong>从请求处理的时效性来看</strong> ：通过 RPC 发出的调用一般会立即被处理，存放在消息队列中的消息并不一定会立即被处理。</li>
</ul>
<p>RPC 和消息队列本质上是网络通讯的两种不同的实现机制，两者的用途不同，万不可将两者混为一谈。</p>
<hr>
<h4 id="消息队列技术选型"><a href="#消息队列技术选型" class="headerlink" title="消息队列技术选型"></a>消息队列技术选型</h4><p><img src="E:\XxdBlog\source_posts\images\test\image-20230408141302450.png" alt="image-20230408141302450"></p>
<p><img src="https://s2.loli.net/2023/04/08/nWqGTubrwzF9xg8.png"></p>
<p><strong>总结：</strong></p>
<ul>
<li>ActiveMQ 的社区算是比较成熟，但是较目前来说，ActiveMQ 的性能比较差，而且版本迭代很慢，不推荐使用，已经被淘汰了。</li>
<li>RabbitMQ 在吞吐量方面虽然稍逊于 Kafka 、RocketMQ 和 Pulsar，但是由于它基于 Erlang 开发，所以并发能力很强，性能极其好，延时很低，达到微秒级。但是也因为 RabbitMQ 基于 Erlang 开发，所以国内很少有公司有实力做 Erlang 源码级别的研究和定制。如果业务场景对并发量要求不是太高（十万级、百万级），那这几种消息队列中，RabbitMQ 或许是你的首选。</li>
<li>RocketMQ 和 Pulsar 支持强一致性，对消息一致性要求比较高的场景可以使用。</li>
<li>RocketMQ 阿里出品，Java 系开源项目，源代码我们可以直接阅读，然后可以定制自己公司的 MQ，并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。</li>
<li>Kafka 的特点其实很明显，就是仅仅提供较少的核心功能，但是提供超高的吞吐量，ms 级的延迟，极高的可用性以及可靠性，而且分布式可以任意扩展。同时 Kafka 最好是支撑较少的 topic 数量即可，保证其超高吞吐量。Kafka 唯一的一点劣势是有可能消息重复消费，那么对数据准确性会造成极其轻微的影响，在大数据领域中以及日志采集中，这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。如果是大数据领域的实时计算、日志采集等场景，用 Kafka 是业内标准的，绝对没问题，社区活跃度很高，绝对不会黄，何况几乎是全世界这个领域的事实性规范。</li>
</ul>
<hr>
<h3 id="Kafka"><a href="#Kafka" class="headerlink" title="Kafka"></a>Kafka</h3><p>![Kafka ](E:\XxdBlog\source_posts\images\test\Kafka .png)<img src="https://s2.loli.net/2023/04/08/4dZ23ukBEoPeU7i.png"></p>
<h4 id="Kafka-是什么？主要应用场景有哪些？"><a href="#Kafka-是什么？主要应用场景有哪些？" class="headerlink" title="Kafka 是什么？主要应用场景有哪些？"></a>Kafka 是什么？主要应用场景有哪些？</h4><p>流平台具有三个关键功能：</p>
<ol>
<li><strong>消息队列</strong>：发布和订阅消息流，这个功能类似于消息队列，这也是 Kafka 也被归类为消息队列的原因。</li>
<li><strong>容错的持久方式存储记录消息流</strong>： Kafka 会把消息持久化到磁盘，有效避免了消息丢失的风险。</li>
<li><strong>流式处理平台：</strong> 在消息发布的时候进行处理，Kafka 提供了一个完整的流式处理类库。</li>
</ol>
<p>Kafka 主要有两大应用场景：</p>
<ol>
<li><strong>消息队列</strong> ：建立实时流数据管道，以可靠地在系统或应用程序之间获取数据。</li>
<li><strong>数据处理：</strong> 构建实时的流数据处理程序来转换或处理数据流</li>
</ol>
<h4 id="和其他消息队列相比-Kafka的优势在哪里？"><a href="#和其他消息队列相比-Kafka的优势在哪里？" class="headerlink" title="和其他消息队列相比,Kafka的优势在哪里？"></a>和其他消息队列相比,Kafka的优势在哪里？</h4><p><strong>极致的性能</strong> ：基于 Scala 和 Java 语言开发，设计中大量使用了批量处理和异步的思想，最高可以每秒处理千万级别的消息。</p>
<p><strong>生态系统兼容性无可匹敌</strong> ：Kafka 与周边生态系统的兼容性是最好的没有之一，尤其在大数据和流计算领域。</p>
<h4 id="什么是Producer、Consumer、Broker、Topic、Partition？"><a href="#什么是Producer、Consumer、Broker、Topic、Partition？" class="headerlink" title="什么是Producer、Consumer、Broker、Topic、Partition？"></a>什么是Producer、Consumer、Broker、Topic、Partition？</h4><p><img src="https://oss.javaguide.cn/github/javaguide/high-performance/message-queue20210507200944439.png" alt="img"></p>
<ol>
<li><p><strong>Producer（生产者）</strong> : 产生消息的一方。</p>
</li>
<li><p><strong>Consumer（消费者）</strong> : 消费消息的一方。</p>
</li>
<li><p><strong>Broker（代理）</strong> : 可以看作是一个独立的 Kafka 实例。多个 Kafka Broker 组成一个 Kafka Cluster。</p>
<ul>
<li><p><strong>Topic（主题）</strong> : Producer 将消息发送到特定的主题，Consumer 通过订阅特定的 Topic(主题) 来消费消息。</p>
</li>
<li><p><strong>Partition（分区）</strong> : Partition 属于 Topic 的一部分。一个 Topic 可以有多个 Partition ，并且同一 Topic 下的 Partition 可以分布在不同的 Broker 上，这也就表明一个 Topic 可以横跨多个 Broker 。这正如我上面所画的图一样<strong>Kafka 中的 Partition（分区） 实际上可以对应成为消息队列中的队列。</strong></p>
</li>
</ul>
</li>
</ol>
<h4 id="Kafka-的多副本机制了解吗？带来了什么好处？"><a href="#Kafka-的多副本机制了解吗？带来了什么好处？" class="headerlink" title="Kafka 的多副本机制了解吗？带来了什么好处？"></a>Kafka 的多副本机制了解吗？带来了什么好处？</h4><p>我们发送的消息会被发送到 leader 副本，然后 follower 副本才能从 leader 副本中拉取消息进行同步。</p>
<blockquote>
<p>生产者和消费者只与 leader 副本交互。你可以理解为其他副本只是 leader 副本的拷贝，它们的存在只是为了保证消息存储的安全性。当 leader 副本发生故障时会从 follower 中选举出一个 leader,但是 follower 中如果有和 leader 同步程度达不到要求的参加不了 leader 的竞选。</p>
</blockquote>
<h5 id="Kafka-的多分区（Partition）以及多副本（Replica）机制有什么好处呢？"><a href="#Kafka-的多分区（Partition）以及多副本（Replica）机制有什么好处呢？" class="headerlink" title="Kafka 的多分区（Partition）以及多副本（Replica）机制有什么好处呢？"></a><strong>Kafka 的多分区（Partition）以及多副本（Replica）机制有什么好处呢？</strong></h5><ul>
<li>Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力&#x3D;&#x3D;（负载均衡）&#x3D;&#x3D;。</li>
<li>Partition 可以指定对应的 Replica 数, 这也极大地提高了消息存储的安全性, &#x3D;&#x3D;提高了容灾能力&#x3D;&#x3D;，不过也相应的增加了所需要的存储空间。</li>
</ul>
<h4 id="Kafka-如何保证消息的消费顺序？"><a href="#Kafka-如何保证消息的消费顺序？" class="headerlink" title="Kafka 如何保证消息的消费顺序？"></a>Kafka 如何保证消息的消费顺序？</h4><ol>
<li>1 个 Topic 只对应一个 Partition。</li>
<li>（推荐）发送消息的时候指定 key&#x2F;Partition。</li>
</ol>
<h4 id="Kafka-如何保证消息不丢失"><a href="#Kafka-如何保证消息不丢失" class="headerlink" title="Kafka 如何保证消息不丢失"></a>Kafka 如何保证消息不丢失</h4><h5 id="生产者丢失消息的情况"><a href="#生产者丢失消息的情况" class="headerlink" title="生产者丢失消息的情况"></a>生产者丢失消息的情况</h5><p>为 Producer 的<code>retries </code>（<strong>重试次数</strong>）设置一个比较合理的值，一般是 3 ，但是为了保证消息不丢失的话一般会设置比较大一点。设置完成之后，当出现网络问题之后能够自动重试消息发送，避免消息丢失。另外，建议还要设置重试间隔，因为间隔太小的话重试的效果就不明显了，网络波动一次你3次一下子就重试完了</p>
<h5 id="消费者丢失消息的情况"><a href="#消费者丢失消息的情况" class="headerlink" title="消费者丢失消息的情况"></a>消费者丢失消息的情况</h5><p><strong>解决办法也比较粗暴，我们手动关闭自动提交 offset，每次在真正消费完消息之后再自己手动提交 offset</strong> 。–&gt;导致消息重复消费</p>
<h5 id="Kafka-弄丢了消息"><a href="#Kafka-弄丢了消息" class="headerlink" title="Kafka 弄丢了消息"></a>Kafka 弄丢了消息</h5><p><strong>假如 leader 副本所在的 broker 突然挂掉，那么就要从 follower 副本重新选出一个 leader ，但是 leader 的数据还有一些没有被 follower 副本的同步的话，就会造成消息丢失。</strong></p>
<ul>
<li><strong>设置 acks &#x3D; all</strong></li>
<li><strong>设置 replication.factor &gt;&#x3D; 3</strong></li>
<li><strong>设置 min.insync.replicas &gt; 1</strong></li>
<li><strong>设置 unclean.leader.election.enable &#x3D; false</strong>（选主放弃不达标的follower）</li>
</ul>
<hr>
<h4 id="Kafka-如何保证消息不重复消费"><a href="#Kafka-如何保证消息不重复消费" class="headerlink" title="Kafka 如何保证消息不重复消费"></a>Kafka 如何保证消息不重复消费</h4><p><strong>kafka出现消息重复消费的原因：</strong></p>
<ul>
<li>服务端侧已经消费的数据没有成功提交 offset（根本原因）。</li>
<li>Kafka 侧 由于服务端处理业务时间长或者网络链接等等原因让 Kafka 认为服务假死，触发了分区 rebalance。</li>
</ul>
<p>解决方案：</p>
<ol>
<li><p>消费消息服务做幂等校验，比如 Redis 的set、MySQL 的主键等天然的幂等功能。这种方法最有效。</p>
</li>
<li><p>将 <strong><code>enable.auto.commit</code></strong> 参数设置为 false，关闭自动提交，开发者在代码中手动提交 offset。那么这里会有个问题：<strong>什么时候提交offset合适？</strong></p>
<ul>
<li><p>处理完消息再提交：依旧有消息重复消费的风险，和自动提交一样</p>
</li>
<li><p>拉取到消息即提交：会有消息丢失的风险。允许消息延时的场景，一般会采用这种方式。然后，通过定时任务在业务不繁忙（比如凌晨）的时候做数据兜底。</p>
</li>
</ul>
</li>
</ol>
<p><img src="E:\XxdBlog\source_posts\images\test\面试问题.png" alt="面试问题"><img src="https://s2.loli.net/2023/04/08/fq7YEDJbCikROlc.png"></p>
<hr>
<h3 id="RocketMQ"><a href="#RocketMQ" class="headerlink" title="RocketMQ"></a>RocketMQ</h3><p><img src="https://s2.loli.net/2023/04/10/3pshYuMU8Qtc9qb.png"></p>
<p><img src="https://s2.loli.net/2023/04/08/waMbRjI6q4TDKEY.png"></p>
<h4 id="主题模型"><a href="#主题模型" class="headerlink" title="主题模型"></a>主题模型</h4><p><code>Producer Group</code> 生产者组： 代表某一类的生产者，比如我们有多个秒杀系统作为生产者，这多个合在一起就是一个 <code>Producer Group</code> 生产者组，它们一般生产相同的消息。</p>
<p><code>Consumer Group</code> 消费者组： 代表某一类的消费者，比如我们有多个短信系统作为消费者，这多个合在一起就是一个 <code>Consumer Group</code> 消费者组，它们一般消费相同的消息。</p>
<p><code>Topic</code> 主题： 代表一类消息，比如订单消息，物流消息等等。</p>
<p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef3857fefaa079.jpg" alt="img"></p>
<p><strong>为什么一个主题中需要维护多个队列</strong> ？</p>
<p>提高并发能力，<strong>使用在一个 <code>Topic</code> 中配置多个队列并且每个队列维护每个消费者组的消费位置</strong> 实现了 <strong>主题模式&#x2F;发布订阅模式</strong> 。</p>
<hr>
<h5 id="RocketMQ的架构图"><a href="#RocketMQ的架构图" class="headerlink" title="RocketMQ的架构图"></a>RocketMQ的架构图</h5><p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef38687488a5a4.jpg" alt="img"></p>
<p><code>RocketMQ</code> 技术架构中有四大角色 <code>NameServer</code> 、<code>Broker</code> 、<code>Producer</code> 、<code>Consumer</code> 。</p>
<ul>
<li><code>Broker</code>： 主要负责消息的存储、投递和查询以及服务高可用保证。( <strong>Producer 发过来的消息、处理 Consumer 的消费消息请求、消息的持 久化存储、消息的 HA 机制以及服务端过滤功能</strong>)，<strong>一个 <code>Topic</code> 分布在多个 <code>Broker</code>上，一个 <code>Broker</code> 可以配置多个 <code>Topic</code> ，它们是多对多的关系</strong>。</li>
<li><code>NameServer</code>：是一个 <strong>注册中心</strong> ，主要提供两个功能：<strong>Broker管理</strong> 和 <strong>路由信息管理</strong> 。</li>
<li><code>Producer</code>： 消息发布的角色，支持分布式集群方式部署。</li>
<li><code>Consumer</code>： 消息消费的角色，支持分布式集群方式部署。</li>
</ul>
<p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef386fa3be1e53.jpg" alt="img"></p>
<p>第一、Broker&#96; <strong>做了集群并且还进行了主从部署</strong> </p>
<p>第二、为了保证 <code>HA</code> ， <code>NameServer</code> 也做了集群部署，但是请注意它是 <strong>去中心化</strong> 的。<strong>单个Broker和所有NameServer保持长连接</strong> ，</p>
<p>第三、在生产者需要向 <code>Broker</code> 发送消息的时候，<strong>需要先从 <code>NameServer</code> 获取关于 <code>Broker</code> 的路由信息</strong>，然后通过 <strong>轮询</strong> 的方法去向每个队列中生产数据以达到 <strong>负载均衡</strong> 的效果。</p>
<p>第四、消费者通过 <code>NameServer</code> 获取所有 <code>Broker</code> 的路由信息后，向 <code>Broker</code> 发送 <code>Pull</code> 请求来获取消息数据。<code>Consumer</code> 可以以两种模式启动—— <strong>广播（Broadcast）和集群（Cluster）</strong>。广播模式下，一条消息会发送给 <strong>同一个消费组中的所有消费者</strong> ，集群模式下消息只会发送给一个消费者。</p>
<hr>
<h4 id="如何解决-顺序消费、重复消费"><a href="#如何解决-顺序消费、重复消费" class="headerlink" title="如何解决 顺序消费、重复消费"></a>如何解决 顺序消费、重复消费</h4><h5 id="顺序消费"><a href="#顺序消费" class="headerlink" title="顺序消费"></a>顺序消费</h5><p><strong><code>RocketMQ</code> 在主题上是无序的、它只有在队列层面才是保证有序</strong></p>
<h6 id="普通顺序"><a href="#普通顺序" class="headerlink" title="普通顺序"></a><strong>普通顺序</strong></h6><p><strong>同一个消费队列收到的消息是有顺序的</strong> ，<code>Broker</code> <strong>重启情况下不会保证消息顺序性</strong></p>
<p><strong>轮询方式</strong></p>
<p>轮询(取决你的负载均衡策略)来向同一主题的不同消息队列发送消息</p>
<p>将同一语义下的消息放入同一个队列(比如这里是同一个订单)，可以使用 <strong>Hash取模法</strong> 来保证同一个订单在同一个队列中就行了。</p>
<h6 id="严格顺序"><a href="#严格顺序" class="headerlink" title="严格顺序"></a><strong>严格顺序</strong></h6><p>消费者收到的 <strong>所有消息</strong> 均是有顺序的。严格顺序消息 <strong>即使在异常情况下也会保证消息的顺序性</strong> 。</p>
<h5 id="重复消费"><a href="#重复消费" class="headerlink" title="重复消费"></a>重复消费</h5><h6 id="幂等"><a href="#幂等" class="headerlink" title="幂等"></a><strong>幂等</strong></h6><ul>
<li><strong>写入 <code>Redis</code></strong></li>
<li><strong>数据库插入法</strong></li>
</ul>
<hr>
<h4 id="分布式事务-1"><a href="#分布式事务-1" class="headerlink" title="分布式事务"></a>分布式事务</h4><h5 id="事务消息加上事务反查机制"><a href="#事务消息加上事务反查机制" class="headerlink" title="事务消息加上事务反查机制"></a><strong>事务消息加上事务反查机制</strong></h5><p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef38798d7a987f.png" alt="img"></p>
<hr>
<h4 id="消息堆积问题"><a href="#消息堆积问题" class="headerlink" title="消息堆积问题"></a>消息堆积问题</h4><h5 id="削峰"><a href="#削峰" class="headerlink" title="削峰"></a>削峰</h5><ul>
<li>降级限流</li>
<li>增加消费者，同时增加每个主题的队列数量</li>
</ul>
<hr>
<h4 id="回溯消费"><a href="#回溯消费" class="headerlink" title="回溯消费"></a>回溯消费</h4><hr>
<h4 id="RocketMQ-的刷盘机制"><a href="#RocketMQ-的刷盘机制" class="headerlink" title="RocketMQ 的刷盘机制"></a>RocketMQ 的刷盘机制</h4><h5 id="同步刷盘和异步刷盘"><a href="#同步刷盘和异步刷盘" class="headerlink" title="同步刷盘和异步刷盘"></a>同步刷盘和异步刷盘</h5><p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/16ef387fba311cda.jpg" alt="img"></p>
<p>在同步刷盘中需要等待一个刷盘成功的 <code>ACK</code> ，同步刷盘对 <code>MQ</code> 消息可靠性来说是一种不错的保障，但是 <strong>性能上会有较大影响</strong> ，一般地适用于金融等特定业务场景。</p>
<p>而异步刷盘往往是开启一个线程去异步地执行刷盘操作。消息刷盘采用后台异步线程提交的方式进行， <strong>降低了读写延迟</strong> ，提高了 <code>MQ</code> 的性能和吞吐量，一般适用于如发验证码等对于消息保证要求不太高的业务场景。</p>
<p>一般地，<strong>异步刷盘只有在 <code>Broker</code> 意外宕机的时候会丢失部分数据</strong>，你可以设置 <code>Broker</code> 的参数 <code>FlushDiskType</code> 来调整你的刷盘策略(ASYNC_FLUSH 或者 SYNC_FLUSH)。</p>
<hr>
<p>而同步复制和异步复制主要是指的 &#x3D;&#x3D;<code>Borker</code> 主从模式&#x3D;&#x3D;下</p>
<h5 id="同步复制和异步复制"><a href="#同步复制和异步复制" class="headerlink" title="同步复制和异步复制"></a>同步复制和异步复制</h5><ul>
<li>同步复制： 也叫 “同步双写”，也就是说，<strong>只有消息同步双写到主从节点上时才返回写入成功</strong> 。</li>
<li>异步复制： <strong>消息写入主节点之后就直接返回写入成功</strong> 。</li>
</ul>
<p><strong>异步复制会不会也像异步刷盘那样影响消息的可靠性呢？</strong></p>
<p>答案是不会的，因为两者就是不同的概念，对于消息可靠性是通过不同的刷盘策略保证的，而像异步同步复制策略仅仅是影响到了 <strong>可用性</strong> 。为什么呢？其主要原因<strong>是 <code>RocketMQ</code> 是不支持自动主从切换的，当主节点挂掉之后，生产者就不能再给这个主节点生产消息了</strong>。</p>
<p>比如这个时候采用异步复制的方式，在主节点还未发送完需要同步的消息的时候主节点挂掉了，这个时候从节点就少了一部分消息。但是此时生产者无法再给主节点生产消息了，<strong>消费者可以自动切换到从节点进行消费</strong>(仅仅是消费)，所以在主节点挂掉的时间只会产生主从结点短暂的消息不一致的情况，降低了可用性，而当主节点重启之后，从节点那部分未来得及复制的消息还会继续复制。</p>
<hr>
<h5 id="存储机制"><a href="#存储机制" class="headerlink" title="存储机制"></a>存储机制</h5><p><code>CommitLog</code>： <strong>消息主体以及元数据的存储主体</strong>。<strong>顺序写入日志文件</strong>。</p>
<p><code>ConsumeQueue</code>： 消息消费队列，<strong>引入的目的主要是提高消息消费的性能</strong>，ConsumeQueue（逻辑消费队列）消费消息的索引，保存在CommitLog中的起始物理偏移量offset，消息大小size和消息Tag的HashCode值。**<code>consumequeue</code> 文件可以看成是基于 <code>topic</code> 的 <code>commitlog</code> 索引文件**</p>
<p><code>IndexFile</code>： <code>IndexFile</code>（索引文件）提供了一种可以通过key或时间区间来查询消息的方法。</p>
<hr>
<p><code>RocketMQ</code> 采用的是 <strong>混合型的存储结构</strong> ，即为 <code>Broker</code> 单个实例下所有的队列共用一个日志数据文件来存储消息。有意思的是在同样高并发的 <code>Kafka</code> 中会为每个 <code>Topic</code> 分配一个存储文件。</p>
<hr>
<h3 id="RabbitMQ"><a href="#RabbitMQ" class="headerlink" title="RabbitMQ"></a>RabbitMQ</h3><p><img src="E:\XxdBlog\source_posts\images\test\RabbitMQ.png" alt="RabbitMQ"></p>
<h4 id="RabbitMQ-介绍"><a href="#RabbitMQ-介绍" class="headerlink" title="RabbitMQ 介绍"></a>RabbitMQ 介绍</h4><h5 id="特点"><a href="#特点" class="headerlink" title="特点"></a>特点</h5><ul>
<li><strong>可靠性：</strong> RabbitMQ使用一些机制来保证消息的可靠性，如持久化、传输确认及发布确认等。</li>
<li><strong>灵活的路由：</strong> 在消息进入队列之前，通过交换器来路由消息。对于典型的路由功能，RabbitMQ 己经提供了一些内置的交换器来实现。针对更复杂的路由功能，可以将多个交换器绑定在一起，也可以通过插件机制来实现自己的交换器。这个后面会在我们讲 RabbitMQ 核心概念的时候详细介绍到。</li>
<li><strong>扩展性：</strong> 多个RabbitMQ节点可以组成一个集群，也可以根据实际业务情况动态地扩展集群中节点。</li>
<li><strong>高可用性：</strong> 队列可以在集群中的机器上设置镜像，使得在部分节点出现问题的情况下队列仍然可用。</li>
<li><strong>支持多种协议：</strong> RabbitMQ 除了原生支持 AMQP 协议，还支持 STOMP、MQTT 等多种消息中间件协议。</li>
<li><strong>多语言客户端：</strong> RabbitMQ几乎支持所有常用语言，比如 Java、Python、Ruby、PHP、C#、JavaScript等。</li>
<li><strong>易用的管理界面：</strong> RabbitMQ提供了一个易用的用户界面，使得用户可以监控和管理消息、集群中的节点等。在安装 RabbitMQ 的时候会介绍到，安装好 RabbitMQ 就自带管理界面。</li>
<li><strong>插件机制：</strong> RabbitMQ 提供了许多插件，以实现从多方面进行扩展，当然也可以编写自己的插件。感觉这个有点类似 Dubbo 的 SPI机制。</li>
</ul>
<hr>
<h4 id="RabbitMQ-核心概念"><a href="#RabbitMQ-核心概念" class="headerlink" title="RabbitMQ 核心概念"></a>RabbitMQ 核心概念</h4><p><img src="https://oss.javaguide.cn/github/javaguide/rabbitmq/96388546.jpg" alt="图1-RabbitMQ 的整体模型架构"></p>
<h5 id="架构"><a href="#架构" class="headerlink" title="架构"></a>架构</h5><h6 id="Producer-生产者-和-Consumer-消费者"><a href="#Producer-生产者-和-Consumer-消费者" class="headerlink" title="Producer(生产者) 和 Consumer(消费者)"></a>Producer(生产者) 和 Consumer(消费者)</h6><ul>
<li><strong>Producer(生产者)</strong> :生产消息的一方（邮件投递者）</li>
<li><strong>Consumer(消费者)</strong> :消费消息的一方（邮件收件人）</li>
</ul>
<p>消息一般由 2 部分组成：<strong>消息头</strong>（或者说是标签 Label）和 <strong>消息体</strong>。消息体也可以称为 payLoad ,消息体是不透明的，而消息头则由一系列的可选属性组成，这些属性包括 routing-key（路由键）、priority（相对于其他消息的优先权）、delivery-mode（指出该消息可能需要持久性存储）等。生产者把消息交由 RabbitMQ 后，RabbitMQ 会根据消息头把消息发送给感兴趣的 Consumer(消费者)。</p>
<h6 id="Exchange-交换器"><a href="#Exchange-交换器" class="headerlink" title="Exchange(交换器)"></a>Exchange(交换器)</h6><p><strong>RabbitMQ 的 Exchange(交换器) 有4种类型，不同的类型对应着不同的路由策略</strong>：<strong>direct(默认)<strong>，</strong>fanout</strong>, <strong>topic</strong>, 和 <strong>headers</strong></p>
<p>生产者将消息发给交换器的时候，一般会指定一个 **RoutingKey(路由键)**，用来指定这个消息的路由规则，而这个 <strong>RoutingKey 需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效</strong>。</p>
<h6 id="Queue-消息队列"><a href="#Queue-消息队列" class="headerlink" title="Queue(消息队列)"></a>Queue(消息队列)</h6><p><strong>多个消费者可以订阅同一个队列</strong>，这时队列中的消息会被平均分摊（Round-Robin，即轮询）给多个消费者进行处理，而不是每个消费者都收到所有的消息并处理，这样避免消息被重复消费。</p>
<p><strong>RabbitMQ</strong> 不支持队列层面的广播消费</p>
<h6 id="Broker（消息中间件的服务节点）"><a href="#Broker（消息中间件的服务节点）" class="headerlink" title="Broker（消息中间件的服务节点）"></a>Broker（消息中间件的服务节点）</h6><p><img src="https://oss.javaguide.cn/github/javaguide/rabbitmq/67952922.jpg" alt="消息队列的运转过程"></p>
<h6 id="Exchange-Types-交换器类型"><a href="#Exchange-Types-交换器类型" class="headerlink" title="Exchange Types(交换器类型)"></a>Exchange Types(交换器类型)</h6><ul>
<li>fanout： 把所有发送到该Exchange的消息路由到所有与它绑定的Queue中，不做任何判断操作，速度最快。fanout 类型常用来广播消息</li>
<li>direct：把消息路由到那些 Bindingkey 与 RoutingKey 完全匹配的 Queue 中。用来处理有优先级的任务<img src="https://oss.javaguide.cn/github/javaguide/rabbitmq/37008021.jpg" alt="direct 类型交换器"></li>
<li>topic:是将消息路由到 BindingKey 和 RoutingKey 相匹配的队列中。用<code>.</code>进行分隔，用<code>*，#</code>表示通配符<img src="https://oss.javaguide.cn/github/javaguide/rabbitmq/73843.jpg" alt="topic 类型交换器"></li>
<li>header(不推荐):根据发送的消息内容中的 headers 属性进行匹配。</li>
</ul>
<h4 id="常见面试问题"><a href="#常见面试问题" class="headerlink" title="常见面试问题"></a>常见面试问题</h4><h5 id="AMQP是什么"><a href="#AMQP是什么" class="headerlink" title="AMQP是什么"></a>AMQP是什么</h5><h6 id="三层协议"><a href="#三层协议" class="headerlink" title="三层协议"></a>三层协议</h6><ul>
<li><strong>Module Layer</strong>:协议最高层，主要定义了一些客户端调用的命令，客户端可以用这些命令实现自己的业务逻辑。</li>
<li><strong>Session Layer</strong>:中间层，主要负责客户端命令发送给服务器，再将服务端应答返回客户端，提供可靠性同步机制和错误处理。</li>
<li><strong>TransportLayer</strong>:最底层，主要传输二进制数据流，提供帧的处理、信道服用、错误检测和数据表示等。</li>
</ul>
<h6 id="三大组件"><a href="#三大组件" class="headerlink" title="三大组件"></a><strong>三大组件</strong></h6><ul>
<li><strong>交换器 (Exchange)</strong> ：消息代理服务器中用于把消息路由到队列的组件。</li>
<li><strong>队列 (Queue)</strong> ：用来存储消息的数据结构，位于硬盘或内存中。</li>
<li><strong>绑定 (Binding)</strong> ：一套规则，告知交换器消息应该将消息投递给哪个队列。</li>
</ul>
<hr>
<h5 id="说说生产者-Producer-和消费者-Consumer"><a href="#说说生产者-Producer-和消费者-Consumer" class="headerlink" title="说说生产者 Producer 和消费者 Consumer?"></a><strong>说说生产者 Producer 和消费者 Consumer?</strong></h5><p><strong>生产者</strong> :</p>
<ul>
<li>消息生产者，就是投递消息的一方。</li>
<li>消息一般包含两个部分：消息体（<code>payload</code>)和标签(<code>Label</code>)。</li>
</ul>
<p><strong>消费者</strong> ：</p>
<ul>
<li>消费消息，也就是接收消息的一方。</li>
<li>消费者连接到 RabbitMQ 服务器，并订阅到队列上。消费消息时只消费消息体，丢弃标签</li>
</ul>
<hr>
<h5 id="说说-Broker-服务节点、Queue-队列、Exchange-交换器？"><a href="#说说-Broker-服务节点、Queue-队列、Exchange-交换器？" class="headerlink" title="说说 Broker 服务节点、Queue 队列、Exchange 交换器？"></a>说说 Broker 服务节点、Queue 队列、Exchange 交换器？</h5><ul>
<li><strong>Broker</strong> ： 可以看做 RabbitMQ 的服务节点。一般请下一个 Broker 可以看做一个 RabbitMQ 服务器。</li>
<li><strong>Queue</strong> :RabbitMQ 的内部对象，用于存储消息。多个消费者可以订阅同一队列，这时队列中的消息会被平摊（轮询）给多个消费者进行处理。</li>
<li><strong>Exchange</strong> : 生产者将消息发送到交换器，由交换器将消息路由到一个或者多个队列中。当路由不到时，或返回给生产者或直接丢弃。</li>
</ul>
<hr>
<h5 id="什么是死信队列？如何导致的？"><a href="#什么是死信队列？如何导致的？" class="headerlink" title="什么是死信队列？如何导致的？"></a>什么是死信队列？如何导致的？</h5><p>当消息在一个队列中变成死信 (<code>dead message</code>) 之后，它能被重新被发送到另一个交换器中，这个交换器就是 DLX，绑定 DLX 的队列就称之为死信队列。</p>
<p><strong>导致的死信的几种原因</strong>：</p>
<ul>
<li>消息被拒（<code>Basic.Reject /Basic.Nack</code>) 且 <code>requeue = false</code>。</li>
<li>消息 TTL 过期。</li>
<li>队列满了，无法再添加。</li>
</ul>
<hr>
<h5 id="什么是延迟队列？RabbitMQ-怎么实现延迟队列？"><a href="#什么是延迟队列？RabbitMQ-怎么实现延迟队列？" class="headerlink" title="什么是延迟队列？RabbitMQ 怎么实现延迟队列？"></a>什么是延迟队列？RabbitMQ 怎么实现延迟队列？</h5><p>延迟队列指的是存储对应的延迟消息，消息被发送以后，并不想让消费者立刻拿到消息，而是等待特定时间后，消费者才能拿到这个消息进行消费。</p>
<ul>
<li>通过RabbitMQ本身队列的特性来实现，需要使用RabbitMQ的死信交换机（Exchange）和消息的存活时间TTL（Time To Live）。</li>
<li>在RabbitMQ 3.5.7及以上的版本提供了一个插件（rabbitmq-delayed-message-exchange）来实现延迟队列功能。同时，插件依赖Erlang&#x2F;OPT 18.0及以上。</li>
</ul>
<hr>
<h5 id="什么是优先级队列？"><a href="#什么是优先级队列？" class="headerlink" title="什么是优先级队列？"></a>什么是优先级队列？</h5><p>可以通过<code>x-max-priority</code>参数来实现优先级队列。不过，当消费速度大于生产速度且 Broker 没有堆积的情况下，优先级显得没有意义。</p>
<hr>
<h5 id="RabbitMQ-有哪些工作模式？"><a href="#RabbitMQ-有哪些工作模式？" class="headerlink" title="RabbitMQ 有哪些工作模式？"></a>RabbitMQ 有哪些工作模式？</h5><ul>
<li>简单模式</li>
<li>work 工作模式</li>
<li>pub&#x2F;sub 发布订阅模式</li>
<li>Routing 路由模式</li>
<li>Topic 主题模式</li>
</ul>
<hr>
<h5 id="RabbitMQ-消息怎么传输？"><a href="#RabbitMQ-消息怎么传输？" class="headerlink" title="RabbitMQ 消息怎么传输？"></a>RabbitMQ 消息怎么传输？</h5><p>由于 TCP 链接的创建和销毁开销较大，且并发数受系统资源限制，会造成性能瓶颈，所以 RabbitMQ 使用信道的方式来传输数据。信道是建立在 TCP 链接上的虚拟链接，且每条 TCP 链接上的信道数量没有限制。就是说 RabbitMQ 在一条 TCP 链接上建立成百上千个信道来达到多个线程处理，这个 TCP 被多个线程共享，每个信道在 RabbitMQ 都有唯一的 ID，保证了信道私有性，每个信道对应一个线程使用。</p>
<hr>
<h6 id="如何保证消息的可靠性？"><a href="#如何保证消息的可靠性？" class="headerlink" title="如何保证消息的可靠性？"></a><strong>如何保证消息的可靠性？</strong></h6><ul>
<li>生产者到 RabbitMQ：事务机制和 Confirm 机制，注意：事务机制和 Confirm 机制是互斥的，两者不能共存，会导致 RabbitMQ 报错。</li>
<li>RabbitMQ 自身：持久化、集群、普通模式、镜像模式。</li>
<li>RabbitMQ 到消费者：basicAck 机制、死信队列、消息补偿机制。</li>
</ul>
<hr>
<h5 id="如何保证-RabbitMQ-消息的顺序性？"><a href="#如何保证-RabbitMQ-消息的顺序性？" class="headerlink" title="如何保证 RabbitMQ 消息的顺序性？"></a>如何保证 RabbitMQ 消息的顺序性？</h5><ul>
<li>拆分多个 queue(消息队列)，每个 queue(消息队列) 一个 consumer(消费者)，就是多一些 queue (消息队列)而已，确实是麻烦点；</li>
<li>或者就一个 queue (消息队列)但是对应一个 consumer(消费者)，然后这个 consumer(消费者)内部用内存队列做排队，然后分发给底层不同的 worker 来处理。</li>
</ul>
<hr>
<h5 id="如何保证-RabbitMQ-高可用的？"><a href="#如何保证-RabbitMQ-高可用的？" class="headerlink" title="如何保证 RabbitMQ 高可用的？"></a>如何保证 RabbitMQ 高可用的？</h5><p><strong>单机模式</strong></p>
<p>Demo 级别的，一般就是你本地启动了玩玩儿的?，没人生产用单机模式。</p>
<p><strong>普通集群模式</strong></p>
<p>意思就是在多台机器上启动多个 RabbitMQ 实例，每个机器启动一个。你创建的 queue，只会放在一个 RabbitMQ 实例上，但是每个实例都同步 queue 的元数据（元数据可以认为是 queue 的一些配置信息，通过元数据，可以找到 queue 所在实例）。</p>
<p>你消费的时候，实际上如果连接到了另外一个实例，那么那个实例会从 queue 所在实例上拉取数据过来。这方案主要是提高吞吐量的，就是说让集群中多个节点来服务某个 queue 的读写操作。</p>
<p><strong>镜像集群模式</strong></p>
<p>这种模式，才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是，在镜像集群模式下，你创建的 queue，无论元数据还是 queue 里的消息都会存在于多个实例上，就是说，每个 RabbitMQ 节点都有这个 queue 的一个完整镜像，包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候，都会自动把消息同步到多个实例的 queue 上。RabbitMQ 有很好的管理控制台，就是在后台新增一个策略，这个策略是镜像集群模式的策略，指定的时候是可以要求数据<strong>同步到所有节点</strong>的，也可以要求<strong>同步到指定数量的节点</strong>，再次创建 queue 的时候，应用这个策略，就会自动将数据同步到其他的节点上去了。</p>
<p>这样的好处在于，你任何一个机器宕机了，没事儿，其它机器（节点）还包含了这个 queue 的完整数据，别的 consumer 都可以到其它节点上去消费数据。坏处在于，第一，这个性能开销也太大了吧，消息需要同步到所有机器上，导致网络带宽压力和消耗很重！RabbitMQ 一个 queue 的数据都是放在一个节点里的，镜像集群下，也是每个节点都放这个 queue 的完整数据。</p>
<hr>
<h5 id="如何解决消息积压问题？"><a href="#如何解决消息积压问题？" class="headerlink" title="如何解决消息积压问题？"></a>如何解决消息积压问题？</h5><p><strong>临时紧急扩容</strong>。先修复 consumer 的问题，确保其恢复消费速度，然后将现有 consumer 都停掉。新建一个 topic，partition 是原来的 10 倍，临时建立好原先 10 倍的 queue 数量。然后写一个临时的分发数据的 consumer 程序，这个程序部署上去消费积压的数据，消费之后不做耗时的处理，直接均匀轮询写入临时建立好的 10 倍数量的 queue。接着临时征用 10 倍的机器来部署 consumer，每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍，以正常的 10 倍速度来消费数据。等快速消费完积压数据之后，得恢复原先部署的架构，重新用原先的 consumer 机器来消费消息。</p>
<hr>
<h5 id="如何解决消息队列的延时以及过期失效问题？"><a href="#如何解决消息队列的延时以及过期失效问题？" class="headerlink" title="如何解决消息队列的延时以及过期失效问题？"></a>如何解决消息队列的延时以及过期失效问题？</h5><p>RabbtiMQ 是可以设置过期时间的，也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉，这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里，而是大量的数据会直接搞丢。我们可以采取一个方案，就是<strong>批量重导</strong>，这个我们之前线上也有类似的场景干过。就是大量积压的时候，我们当时就直接丢弃数据了，然后等过了高峰期以后，比如大家一起喝咖啡熬夜到晚上 12 点以后，用户都睡觉了。这个时候我们就开始写程序，将丢失的那批数据，写个临时程序，一点一点的查出来，然后重新灌入 mq 里面去，把白天丢的数据给他补回来。也只能是这样了。假设 1 万个订单积压在 mq 里面，没有处理，其中 1000 个订单都丢了，你只能手动写程序把那 1000 个订单给查出来，手动发到 mq 里去再补一次。</p>
<hr>
<hr>
<h1 id="高可用"><a href="#高可用" class="headerlink" title="高可用"></a>高可用</h1><h2 id="高可用系统设计指南"><a href="#高可用系统设计指南" class="headerlink" title="高可用系统设计指南"></a>高可用系统设计指南</h2><hr>
<h2 id="冗余设计详解"><a href="#冗余设计详解" class="headerlink" title="冗余设计详解"></a>冗余设计详解</h2><p>对于服务来说，冗余的思想就是相同的服务部署多份，如果正在使用的服务突然挂掉的话，系统可以很快切换到备份服务上，大大减少系统的不可用时间，提高系统的可用性。</p>
<p>对于数据来说，冗余的思想就是相同的数据备份多份，这样就可以很简单地提高数据的安全性。</p>
<hr>
<p>高可用集群（High Availability Cluster，简称 HA Cluster）、同城灾备、异地灾备、同城多活和异地多活是冗余思想在高可用系统设计中最典型的应用。</p>
<ul>
<li><strong>高可用集群</strong> : 同一份服务部署两份或者多份，当正在使用的服务突然挂掉的话，可以切换到另外一台服务，从而保证服务的高可用。</li>
<li><strong>同城灾备</strong> ：一整个集群可以部署在同一个机房，而同城灾备中相同服务部署在同一个城市的不同机房中。并且，备用服务不处理请求。这样可以避免机房出现意外情况比如停电、火灾。</li>
<li><strong>异地灾备</strong> ：类似于同城灾备，不同的是，相同服务部署在异地（通常距离较远，甚至是在不同的城市或者国家）的不同机房中</li>
<li><strong>同城多活</strong> ：类似于同城灾备，但备用服务可以处理请求，这样可以充分利用系统资源，提高系统的并发。</li>
<li><strong>异地多活</strong> : 将服务部署在异地的不同机房中，并且，它们可以同时对外提供服务。</li>
</ul>
<p>高可用集群单纯是服务的冗余，并没有强调地域。同城灾备、异地灾备、同城多活和异地多活实现了地域上的冗余。</p>
<hr>
<h2 id="服务限流详解"><a href="#服务限流详解" class="headerlink" title="服务限流详解"></a>服务限流详解</h2><p>针对软件系统来说，限流就是对请求的速率进行限制，避免瞬时的大量请求击垮软件系统。毕竟，软件系统的处理能力是有限的。如果说超过了其处理能力的范围，软件系统可能直接就挂掉了。</p>
<h3 id="常见限流算法有哪些？"><a href="#常见限流算法有哪些？" class="headerlink" title="常见限流算法有哪些？"></a>常见限流算法有哪些？</h3><h4 id="固定窗口计数器算法"><a href="#固定窗口计数器算法" class="headerlink" title="固定窗口计数器算法"></a><strong>固定窗口计数器算法</strong></h4><p>固定窗口其实就是时间窗口。<strong>固定窗口计数器算法</strong> 规定了我们单位时间处理的请求数量。</p>
<p>假如我们规定系统中某个接口 1 分钟只能访问 33 次的话，使用固定窗口计数器算法的实现思路如下：</p>
<ul>
<li>给定一个变量 <code>counter</code> 来记录当前接口处理的请求数量，初始值为 0（代表接口当前 1 分钟内还未处理请求）。</li>
<li>1 分钟之内每处理一个请求之后就将 <code>counter+1</code> ，当 <code>counter=33</code> 之后（也就是说在这 1 分钟内接口已经被访问 33 次的话），后续的请求就会被全部拒绝。</li>
<li>等到 1 分钟结束后，将 <code>counter</code> 重置 0，重新开始计数。</li>
</ul>
<p><strong>这种限流算法无法保证限流速率，因而无法保证突然激增的流量。</strong></p>
<hr>
<h4 id="滑动窗口计数器算法"><a href="#滑动窗口计数器算法" class="headerlink" title="滑动窗口计数器算法"></a><strong>滑动窗口计数器算法</strong></h4><p>滑动窗口计数器算法相比于固定窗口计数器算法的优化在于：<strong>它把时间以一定比例分片</strong> 。</p>
<p>例如我们的接口限流每分钟处理 60 个请求，我们可以把 1 分钟分为 60 个窗口。每隔 1 秒移动一次，每个窗口一秒只能处理 不大于 <code>60(请求数)/60（窗口数）</code> 的请求， 如果当前窗口的请求计数总和超过了限制的数量的话就不再处理其他请求。</p>
<p>很显然， <strong>当滑动窗口的格子划分的越多，滑动窗口的滚动就越平滑，限流的统计就会越精确。</strong></p>
<hr>
<h4 id="漏桶算法"><a href="#漏桶算法" class="headerlink" title="漏桶算法"></a><strong>漏桶算法</strong></h4><p>我们可以把发请求的动作比作成注水到桶中，我们处理请求的过程可以比喻为漏桶漏水。我们往桶中以任意速率流入水，以一定速率流出水。当水超过桶流量则丢弃，因为桶容量是不变的，保证了整体的速率。</p>
<p>如果想要实现这个算法的话也很简单，准备一个队列用来保存请求，然后我们定期从队列中拿请求来执行就好了（和消息队列削峰&#x2F;限流的思想是一样的）。</p>
<p><img src="https://static001.infoq.cn/resource/image/75/03/75938d1010138ce66e38c6ed0392f103.png" alt="漏桶算法"></p>
<hr>
<h4 id="令牌桶算法"><a href="#令牌桶算法" class="headerlink" title="令牌桶算法"></a><strong>令牌桶算法</strong></h4><p>令牌桶算法也比较简单。和漏桶算法算法一样，我们的主角还是桶（这限流算法和桶过不去啊）。不过现在桶里装的是令牌了，请求在被处理之前需要拿到一个令牌，请求处理完毕之后将这个令牌丢弃（删除）。我们根据限流大小，按照一定的速率往桶里添加令牌。如果桶装满了，就不能继续往里面继续添加令牌了。</p>
<p><img src="https://static001.infoq.cn/resource/image/ec/93/eca0e5eaa35dac938c673fecf2ec9a93.png" alt="令牌桶算法"></p>
<hr>
<h3 id="单机限流怎么做？"><a href="#单机限流怎么做？" class="headerlink" title="单机限流怎么做？"></a>单机限流怎么做？</h3><p>单机限流可以直接使用 Google Guava 自带的限流工具类 <code>RateLimiter</code> 。 <code>RateLimiter</code> 基于令牌桶算法，可以应对突发流量。除了最基本的令牌桶算法(平滑突发限流)实现之外，Guava 的<code>RateLimiter</code>还提供了 <strong>平滑预热限流</strong> 的算法实现。</p>
<hr>
<h3 id="分布式限流怎么做？"><a href="#分布式限流怎么做？" class="headerlink" title="分布式限流怎么做？"></a>分布式限流怎么做？</h3><p>分布式限流常见的方案：</p>
<ul>
<li><strong>借助中间件架限流</strong> ：可以借助 Sentinel 或者使用 Redis 来自己实现对应的限流逻辑。</li>
<li><strong>网关层限流</strong> ：比较常用的一种方案，直接在网关层把限流给安排上了。不过，通常网关层限流通常也需要借助到中间件&#x2F;框架。就比如 Spring Cloud Gateway 的分布式限流实现<code>RedisRateLimiter</code>就是基于 Redis+Lua 来实现的，再比如 Spring Cloud Gateway 还可以整合 Sentinel 来做限流。</li>
</ul>
<p><strong>为什么建议 Redis+Lua 的方式？</strong> 主要有两点原因：</p>
<ul>
<li><strong>减少了网络开销</strong> ：我们可以利用 Lua 脚本来批量执行多条 Redis 命令，这些 Redis 命令会被提交到 Redis 服务器一次性执行完成，大幅减小了网络开销。</li>
<li><strong>原子性</strong> ：一段 Lua 脚本可以视作一条命令执行，一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行，保证了操作不会被其他指令插入或打扰。</li>
</ul>
<hr>
<h2 id="超时-amp-重试详解"><a href="#超时-amp-重试详解" class="headerlink" title="超时&amp;重试详解"></a>超时&amp;重试详解</h2><p>为了最大限度的减小系统或者服务出现故障之后带来的影响，我们需要用到的 <strong>超时（Timeout）</strong> 和 <strong>重试（Retry）</strong> 机制。</p>
<hr>
<h3 id="超时机制"><a href="#超时机制" class="headerlink" title="超时机制"></a>超时机制</h3><h4 id="什么是超时机制？"><a href="#什么是超时机制？" class="headerlink" title="什么是超时机制？"></a>什么是超时机制？</h4><p>超时机制说的是当一个请求超过指定的时间（比如 1s）还没有被处理的话，这个请求就会直接被取消并抛出指定的异常或者错误（比如 <code>504 Gateway Timeout</code>）。</p>
<p>我们平时接触到的超时可以简单分为下面 2 种：</p>
<ul>
<li><strong>连接超时（ConnectTimeout）</strong> ：客户端与服务端建立连接的最长等待时间。</li>
<li><strong>读取超时（ReadTimeout）</strong> ：客户端和服务端已经建立连接，客户端等待服务端处理完请求的最长时间。实际项目中，我们关注比较多的还是读取超时。</li>
</ul>
<hr>
<h4 id="超时时间应该如何设置？"><a href="#超时时间应该如何设置？" class="headerlink" title="超时时间应该如何设置？"></a>超时时间应该如何设置？</h4><p>如果设置太高的话，会降低超时机制的有效性，系统依然可能会出现大量慢请求堆积的问题。如果设置太低的话，就可能会导致在系统或者服务在某些处理请求速度变慢的情况下（比如请求突然增多），大量请求重试（超时通常会结合重试）继续加重系统或者服务的压力，进而导致整个系统或者服务被拖垮的问题。</p>
<p>通常情况下，我们建议读取超时设置为 <strong>1500ms</strong> ,这是一个比较普适的值。</p>
<hr>
<h3 id="重试机制"><a href="#重试机制" class="headerlink" title="重试机制"></a>重试机制</h3><h4 id="什么是重试机制？"><a href="#什么是重试机制？" class="headerlink" title="什么是重试机制？"></a>什么是重试机制？</h4><p>重试机制一般配合超时机制一起使用，指的是多次发送相同的请求来避免瞬态故障和偶然性故障。</p>
<p>瞬态故障可以简单理解为某一瞬间系统偶然出现的故障，并不会持久。偶然性故障可以理解为哪些在某些情况下偶尔出现的故障，频率通常较低。</p>
<p>重试的核心思想是通过消耗服务器的资源来尽可能获得请求更大概率被成功处理。由于瞬态故障和偶然性故障是很少发生的，因此，重试对于服务器的资源消耗几乎是可以被忽略的。</p>
<hr>
<h4 id="重试的次数如何设置？"><a href="#重试的次数如何设置？" class="headerlink" title="重试的次数如何设置？"></a>重试的次数如何设置？</h4><p>超时和重试机制在实际项目中使用的话，需要注意保证同一个请求没有被多次执行。</p>
<p>什么情况下会出现一个请求被多次执行呢？客户端等待服务端完成请求完成超时但此时服务端已经执行了请求，只是由于短暂的网络波动导致响应在发送给客户端的过程中延迟了。</p>
<hr>
<h2 id="性能测试入门"><a href="#性能测试入门" class="headerlink" title="性能测试入门"></a>性能测试入门</h2><p><img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/%E7%BD%91%E7%AB%99%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95.png" alt="img"></p>
<h3 id="性能测试的指标"><a href="#性能测试的指标" class="headerlink" title="性能测试的指标"></a>性能测试的指标</h3><h4 id="响应时间"><a href="#响应时间" class="headerlink" title="响应时间"></a>响应时间</h4><p><strong>响应时间就是用户发出请求到用户收到系统处理结果所需要的时间。</strong></p>
<p>比较出名的 2-5-8 原则是这样描述的：通常来说，2到5秒，页面体验会比较好，5到8秒还可以接受，8秒以上基本就很难接受了。另外，据统计当网站慢一秒就会流失十分之一的客户</p>
<hr>
<h4 id="并发数"><a href="#并发数" class="headerlink" title="并发数"></a>并发数</h4><p><strong>并发数是系统能同时处理请求的数目即同时提交请求的用户数目。</strong></p>
<p>不得不说，高并发是现在后端架构中非常非常火热的一个词了，这个与当前的互联网环境以及中国整体的互联网用户量都有很大关系。一般情况下，你的系统并发量越大，说明你的产品做的就越大。但是，并不是每个系统都需要达到像淘宝、12306 这种亿级并发量的。</p>
<hr>
<h4 id="吞吐量"><a href="#吞吐量" class="headerlink" title="吞吐量"></a>吞吐量</h4><p>吞吐量指的是系统单位时间内系统处理的请求数量。衡量吞吐量有几个重要的参数：QPS（TPS）、并发数、响应时间。</p>
<ol>
<li>QPS（Query Per Second）：服务器每秒可以执行的查询次数；</li>
<li>TPS（Transaction Per Second）：服务器每秒处理的事务数（这里的一个事务可以理解为客户发出请求到收到服务器的过程）；</li>
<li>并发数；系统能同时处理请求的数目即同时提交请求的用户数目。</li>
<li>响应时间： 一般取多次请求的平均响应时间</li>
</ol>
<p>理清他们的概念，就很容易搞清楚他们之间的关系了。</p>
<ul>
<li><strong>QPS（TPS）</strong> &#x3D; 并发数&#x2F;平均响应时间</li>
<li><strong>并发数</strong> &#x3D; QPS*平均响应时间</li>
</ul>
<p>书中是这样描述 QPS 和 TPS 的区别的。</p>
<blockquote>
<p>QPS vs TPS：QPS 基本类似于 TPS，但是不同的是，对于一个页面的一次访问，形成一个TPS；但一次页面请求，可能产生多次对服务器的请求，服务器对这些请求，就可计入“QPS”之中。如，访问一个页面会请求服务器2次，一次访问，产生一个“T”，产生2个“Q”。</p>
</blockquote>
<hr>
<h4 id="性能计数器"><a href="#性能计数器" class="headerlink" title="性能计数器"></a>性能计数器</h4><p><strong>性能计数器是描述服务器或者操作系统的一些数据指标如内存使用、CPU使用、磁盘与网络I&#x2F;O等情况。</strong></p>
<hr>
<h3 id="几种常见的性能测试"><a href="#几种常见的性能测试" class="headerlink" title="几种常见的性能测试"></a>几种常见的性能测试</h3><h4 id="性能测试"><a href="#性能测试" class="headerlink" title="性能测试"></a>性能测试</h4><p>性能测试方法是通过测试工具模拟用户请求系统，目的主要是为了测试系统的性能是否满足要求。通俗地说，这种方法就是要在特定的运行条件下验证系统的能力状态。</p>
<p>性能测试是你在对系统性能已经有了解的前提之后进行的，并且有明确的性能指标。</p>
<hr>
<h4 id="负载测试"><a href="#负载测试" class="headerlink" title="负载测试"></a>负载测试</h4><p>对被测试的系统继续加大请求压力，直到服务器的某个资源已经达到饱和了，比如系统的缓存已经不够用了或者系统的响应时间已经不满足要求了。</p>
<p>负载测试说白点就是测试系统的上限。</p>
<hr>
<h4 id="压力测试"><a href="#压力测试" class="headerlink" title="压力测试"></a>压力测试</h4><p>不去管系统资源的使用情况，对系统继续加大请求压力，直到服务器崩溃无法再继续提供服务。</p>
<hr>
<h4 id="稳定性测试"><a href="#稳定性测试" class="headerlink" title="稳定性测试"></a>稳定性测试</h4><p>模拟真实场景，给系统一定压力，看看业务是否能稳定运行。</p>
<hr>

      
    </div>
    <footer class="article-footer">
      <a data-url="https://gitee.com/xxditem/2023/04/11/%E9%9D%A2%E8%AF%95/" data-id="clgbksfcw0002o8vx0inhbfni" data-title="test" class="article-share-link">Share</a>
      
      
      
    </footer>
  </div>
  
    
<nav id="article-nav">
  
  
    <a href="/xxd-blog/2023/04/07/%E6%88%91%E7%9A%84%E7%AC%AC%E4%B8%80%E7%AF%87%E5%8D%9A%E5%AE%A2/" id="article-nav-older" class="article-nav-link-wrap">
      <strong class="article-nav-caption">Older</strong>
      <div class="article-nav-title">我的第一篇博客</div>
    </a>
  
</nav>

  
</article>


</section>
        
          <aside id="sidebar">
  
    

  
    

  
    
  
    
  <div class="widget-wrap">
    <h3 class="widget-title">Archives</h3>
    <div class="widget">
      <ul class="archive-list"><li class="archive-list-item"><a class="archive-list-link" href="/xxd-blog/archives/2023/04/">April 2023</a></li></ul>
    </div>
  </div>


  
    
  <div class="widget-wrap">
    <h3 class="widget-title">Recent Posts</h3>
    <div class="widget">
      <ul>
        
          <li>
            <a href="/xxd-blog/2023/04/11/%E9%9D%A2%E8%AF%95/">test</a>
          </li>
        
          <li>
            <a href="/xxd-blog/2023/04/07/%E6%88%91%E7%9A%84%E7%AC%AC%E4%B8%80%E7%AF%87%E5%8D%9A%E5%AE%A2/">我的第一篇博客</a>
          </li>
        
          <li>
            <a href="/xxd-blog/2023/04/07/hello-world/">Hello World</a>
          </li>
        
      </ul>
    </div>
  </div>

  
</aside>
        
      </div>
      <footer id="footer">
  
  <div class="outer">
    <div id="footer-info" class="inner">
      
      &copy; 2023 John Doe<br>
      Powered by <a href="https://hexo.io/" target="_blank">Hexo</a>
    </div>
  </div>
</footer>

    </div>
    <nav id="mobile-nav">
  
    <a href="/xxd-blog/" class="mobile-nav-link">Home</a>
  
    <a href="/xxd-blog/archives" class="mobile-nav-link">Archives</a>
  
</nav>
    


<script src="/xxd-blog/js/jquery-3.4.1.min.js"></script>



  
<script src="/xxd-blog/fancybox/jquery.fancybox.min.js"></script>




<script src="/xxd-blog/js/script.js"></script>





  </div>
</body>
</html>